commit 2795213dda6cf376f90fb841caf36b9869b47e96 Author: choibk Date: Mon Jan 5 15:54:29 2026 +0900 Initial clean commit (reset history) diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e4c844d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/redmine_dmsf.iml +Gemfile.lock +.history +tmp/ diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..d685aa21 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,147 @@ +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +plugins: + - rubocop-rails + - rubocop-performance + +AllCops: + TargetRubyVersion: 3.2 + TargetRailsVersion: 7.1 + SuggestExtensions: false + + NewCops: enable + + Exclude: + - '**/vendor/**/*' + +# Rules for DMSF +Layout/LineLength: + Exclude: + - app/models/dmsf_query.rb + +Lint/SymbolConversion: + Exclude: + - test/unit/dmsf_file_revision_test.rb + +Lint/ScriptPermission: + Exclude: + - extra/xapian_indexer.rb + +Lint/UselessAssignment: + Exclude: + - lib/redmine_dmsf/lockable.rb + +Naming/BlockForwarding: + EnforcedStyle: explicit + +Metrics/AbcSize: + Enabled: false + +Metrics/BlockLength: + Enabled: false + +Metrics/BlockNesting: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Metrics/ParameterLists: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Naming/AccessorMethodName: + Exclude: + - lib/dav4rack/resource.rb + +Naming/PredicatePrefix: + Exclude: + - patches/attachable_patch.rb + +Style/HashSyntax: + EnforcedShorthandSyntax: either + +Style/ZeroLengthPredicate: + Exclude: + - lib/redmine_dmsf/webdav/dmsf_resource.rb + +Rails/BulkChangeTable: + Exclude: + - db/migrate/20170217141601_add_dmsf_not_inheritable_to_custom_fields.rb + +Rails/DangerousColumnNames: + Exclude: + - db/migrate/20170330131901_create_dmsf_folder_permissions.rb + +Rails/DynamicFindBy: + AllowedMethods: + - find_by_token + - find_by_param + +Rails/SkipsModelValidations: + Exclude: + - app/helpers/dmsf_upload_helper.rb # touch is Okay + - app/models/dmsf_workflow.rb # update doesn't work here + - lib/redmine_dmsf/patches/user_patch.rb + - lib/redmine_dmsf/patches/role_patch.rb + - db/migrate/20170526144701_dmsf_attachable.rb + - db/migrate/20170421101901_dmsf_file_container_rollback.rb + - db/migrate/20170118142001_dmsf_file_container.rb + - db/migrate/20160222140401_approval_workflow_std_fields.rb + - db/migrate/20160215125801_approval_workflow_status.rb + - db/migrate/20140519133201_trash_bin.rb + - db/migrate/07_dmsf_1_4_4.rb + - db/migrate/20240829093801_rename_dmsf_digest_token.rb + +Rails/ThreeStateBooleanColumn: + Exclude: + - db/migrate/04_dmsf_0_9_0.rb + - db/migrate/20170217141601_add_dmsf_not_inheritable_to_custom_fields.rb + +Rails/UniqueValidationWithoutIndex: + Exclude: + - app/models/dmsf_folder.rb # TODO: Create a case insensitive index in DB migrations + - app/models/dmsf_file.rb + - app/models/dmsf_workflow_step.rb # Impossible due to steps sorting + +Style/ExpandPathArguments: + Enabled: false + +Style/FrozenStringLiteralComment: + Exclude: + - init.rb + +Style/OpenStructUse: + Exclude: + - lib/dav4rack/utils.rb + +Style/OptionalBooleanParameter: + Exclude: + - lib/redmine_dmsf/field_formats/dmsf_file_format.rb + - lib/redmine_dmsf/field_formats/dmsf_file_revision_format.rb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..fc84576f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1256 @@ +Changelog for Redmine DMSF +========================== + +4.2.3 *2025-10-06* +------------------ + + Redmine 6.1 compatibility + +* Bug: #11 - DMSF json API wrong pagination handling +* Bug: #7 - Document edit via WebDAV sets workflow into undefined state + +4.2.2 *2025-07-22* +------------------ + Missing lock icons + +* Bug: #5 - Missing lock icons by locked objects + +4.2.1 *2025-07-10* +------------------ + + Searching in multiple revisions + +IMPORTANT: Issue tracking numbering has been restarted after the movement to the new repository + +* Bug: #1 - Can't install 3.2.4 on redmine 5.0 +* Bug: #3 - Download CSV file leads to 404 +* New: #4 - Searching in multiple revisions + +4.2.0 *2025-07-04* +------------------ + + SQL server compatibility + Change of the license from GNU GPL v2 to v3 + PDF thumbnails + +NOTE: These issues are still from the original danmunn's repository + +* Bug: #1601 - Serialization of attached documents is wrong +* Bug: #1600 - Different versions for different types of uploading +* Bug: #1599 - Empty Trash => Error 500 +* New: #1597 - GNU GPL v2 -> v3 +* New: #1596 - Add a warning about searching in sub-folders +* Bug: #1595 - DMSF WebDAV Configuration Fails on Redmine 6.0.5 + +4.1.3 *2025-05-09* +------------------ + + SQL server compatibility + +* Bug: #1595 - DMSF WebDAV Configuration Fails on Redmine 6.0.5 + +4.1.2 *2025-04-02* +------------------ + + Wrong workflow text by linked documents + +4.1.1 *2025-02-19* +------------------ + + Links download error fix + +* Bug: #1592 - Link entries download error + +4.1.0 *2025-02-12* +------------------ + + Puma 6 compatibility + +* New: #1589 - Support for Puma web server + +4.0.3 *2025-01-31* +------------------ + + Security enhancement + +* Bug: #1370 - Where to report security issues? + +4.0.2 *2025-01-30* +------------------ + + Moving folders fix + Coloured system sub-folders + +* Bug: #1585 - Move X Copy +* Bug: #1580 - Can't upgrade plugin to 4.0.1 after upgrading redmine to 6.0.2 +* Bug: #1557 - Editing of locked folder + +4.0.1 *2025-01-17* +------------------ + + Coloured icons + +* Bug: #1578 - Attached documents are not visible by issues +* Bug: #1577 - Inconsistent default values in settings +* Bug: #1576 - Issue notifications +* New: #1575 - Coloured icons +* Bug: #1574 - Missing default browser context menu +* Bug: #1572 - RAILS_ENV=production bundle exec rake redmine:plugins:migrate NAME=redmine_dmsf fails + +4.0.0 *2024-12-17* +------------------ + + Redmine 6 + +* New: #1571 - Update de.yml +* Bug: #1570 - Emails are not sent to the author if no_self_notified +* Bug: #1568 - The "Link from" doesn't work after upgrading the browsers +* Bug: #1567 - Fixes Column 'dmsf_mail_notification' cannot be null +* Bug: #1566 - Approval workflow notifications +* Bug: #1565 - Plugin settings "Display notified recipients" does not apply! +* Bug: #1564 - Revision API issues +* New: #1563 - Redmine 6 +* New: #1561 - Add a waring about max number of uploaded files exceeded +* Bug: #1560 - Can't add more than one link to an issue + +3.2.4 *2024-10-24* +------------------ + + Multiple file upload fix + Uploaded file size fix + +* Bug: #1559 - Multiple files upload +* Bug: #1558 - Deleting of uploaded files +* Bug: 1556 - Wrong file size when uploading documents + +3.2.2 *2024-10-09* +------------------ + + Upload and Commit in one step + Documents' details in issue form + +* Bug: #1553 - Wiki Tool always in English after installing DMSF Plugin +* New: #1552 - Adds further text to reset button of webdav digest +* Bug: #1551 - Changes token action name for WebDAV digest +* Bug: #1550 - Some controller hooks won't get loaded +* New: #1548 - Document's details in issue form +* Bug: #1544 - Delete uploaded file and upload a different file + +3.2.1 *2024-09-02* +------------------ + + DMSF digest fix + Redmine finance plugin compatibility + +* Bug: #1541 - Missing Digest +* Bug: #1540 - Activity stream is not showing the document name +* Bug: #1424 - Internal error while opening Settings page + +3.2.0 *2024-04-23* +------------------ + + Redmine Product plugin compatibility + +* Bug: #1537 - SystemStackError when Redmine Products plugin installed + +3.1.9 *2024-07-18* +------------------ + + Maintenance release + +* Bug: #1534 - Formating is not applied to Comment column + + +3.1.8 *2024-07-04* +------------------ + + German translation update + Several bugs fixed + +* Bug: #1533 - Mysql2::Error::TimeoutError +* Bug: #1532 - Target folder and project are the same as current +* Bug: #1531 - Fixing NoMethodError in DmsfFileRevisionFormat +* New: #1529 - Maintenance/update german translation + +3.1.7 *2024-06-28* +------------------ + + Maintenance release + +* Bug: #1528 - WebDAV / LDAP-User errors + +3.1.6 *2024-06-04* +------------------ + +* Bug: #1526 - Missing template, responding with 404: Missing partial twofa_backup_codes/_sidebar, application/_sidebar + +3.1.5 *2024-06-04* +------------------ + + WebDAV digest authentication + Inline displaying of text files + +* Bug: #1522 - [ ] in webdav paths seem to make issues with ms-office products +* Bug: #1518 - Mailer partial causes deprecation warning: Rendering actions with '.' in the name is deprecated +* Bug: #1517 - The file is not uploaded to the custom file field +* New: #1502 - Office files preview OK, but text and markdown file downloaded directly +* New: #1464 - Basic authentication sign-in prompts are blocked by default in Microsoft 365 Apps + +3.1.4 *2024-05-06* +------------------ + + Extended issue email notifications + +* New: #1515 - Adds missing safe_attribute for dmsf_not_inheritable +* New: #1511 - Adds further validations +* Bug: #1510 - Uncaught SyntaxError: Redeclaration of let modal in Approval Workflow Log +* Bog: #1506 - Adminstration-settings cannot open after dmsf installed +* Bug: #1505 - Adds an extra check in DmsfQuery#dmsf_node +* New: #1503 - Issue notifications +* Bug: #1501 - Problem clicking action menu (three dots) in DMS file grid +* Bug: #1500 - Non-admin user: NoMethodError for intersect if running on Ruby 2.7.6 +* Bug: #1499 - Allow access only to xxx group , access Internal error 500 +* Bug: #1495 - DMSF doesn't ignore filepattern when LOCK and UNLOCK requests +* Bug: #1494 - Not sending documents when sender is set to user +* Bug: #1493 - Missing translation :notice_account_unknown_email +* Bug: #1491 - Empty system folders + +3.1.3 *2023-11-15* +------------------ + + DMS Document revision as a new custom field type + Copy/Move of multiple entries + REST API + Entries operation (copy, move, download, delete) + Compatibility with Redmine 5.1 + +IMPORTANT: REST API for copying/moving has changed. Check *extra/api/api_client.sh*. + +* Bug: #1490 - Latest plugin version on windows: problematic dependency 'xapian-ruby' +* Bug: #1486 - Some context menu improvements +* Bug: #1485 - Renames locales/ua.yml +* Bug: #1484 - Author should be kept when moving a folder type +* Bug: #1483 - Setting.plugin_redmine_dmsf['dmsf_index_database']: undefined method 'strip' for nil:NilClass +* Bug: #1479 - Cannot uninstall plugin +* Bug: #1477 - Watch permission won't work +* New: #1474 - Removes main menu from workflows controller +* Bug: #1473 - Edited documents cannot be unlocked +* Bug: #1472 - Failed upgrade up to version 3.1.1 from version 3.0.12 +* New: #1248 - Make DMS document available as Type of a custom field +* New: #1132 - Please provide a simple file operation menu + +3.1.2 *2023-08-23* +------------------ + + Bug fixing + +* Bug #1469 - Can't access a folder under watch, but works again when unwatching the folder + +3.1.1 *2023-08-17* +------------------ + + Bug fixing + +* Bug #1466 - Wrong number of arguments in dmsf links new + +3.1.0 *2023-08-10* +------------------ + + Compatibility with Redmine 5.1 + Ukrainian translation + +* New: #1461 - I want to add a Ukrainian translation to this wonderful plugin +* Bug: #1459 - Fix .zero? error in id_attribute check +* New: #1458 - V3.0.13 update error: Error in bundle plugins vault&dashboard + +3.0.13 *2023-06-21* +------------------ + + Italian and German localization updated + OCR supported in full-text search + Source codes of the plugin are checked with Rubocop + +* Bug: #1457 - Wiki Macros present a 'http' link regardless of whether redmine is configured for http or https +* New: #1455 - (Partially) updated IT translation +* Bug: #1454 - After file convert view permissions missing +* Bug: #1453 - I downloaded files from devel branch and put them into plugin folder. Then I run +* Bug: #1452 - API call "commit" not accepting "custom_version_major", "custom_version_minor" anymore? +* Bug: #1449 - Lost attachment on bulk edit +* Bug: #1448 - Convert documents fails +* New: #1445 - To support OCR feature +* Bug: #1444 - Feature/add notification labels +* New: #1443 - Updates german translations +* Bug: #1439 - Error when opening Setting page +* Bug: #1438 - Error while de-installing the plugin "Validation failed: Name contains invalid character(s)" +* Bug: #1434 - File view permissions issue + +3.0.12 *2023-03-15* +------------------ + + Bug fixing + +* Bug: #1436 - Cannot upload new content + +3.0.11 *2023-03-14* +------------------ + + Bug fixing + +3.0.10 *2023-03-10* +------------------- + + Maintenance release + +* Bug: #1433 - User's guide broken link +* Bug: #1426 - Adds formatting helper to all editors + +3.0.9 *2023-02-10* +------------------ + + Sorting + Filtering by custom fields + Download notifications + Embedded help + DMS macros in wiki toolbar + +* Bug: #1425 - Default sorting is not set +* Bug: #1424 - Internal error while opening Settings page +* Bug: #1423 - Check for Illegal characters in file name +* New: #1421 - Help +* Bug: #1419 - Missing checksum +* Bug: #1417 - Query::StatementInvalid raised in dmsf#show when filtering custom fields with PostgreSQL database +* New: #1414 - An empty minor version +* Bug: #1413 - Vim edit through webdav causes lose of all file versions besides last. +* Bug: #1408 - Lost attachment 2 +* New: #513 - Email Notification when someone downloads a file +* New: #239 - Easy Document link macro creation + +3.0.7 *2022-11-01* +------------------ + + AlphaNode's plugins compatibility + Approval workflow enhancement + Global search + New filters + +* Bug: #1407 - Error on "New step or New Approver" +* New: #1247 - Global DMS view - Search by title does not work +* New: #1192 - Suggest to show Approval option in list view instead hiding it in second layer menu being folded +* New: #1124 - New UI: Add additional filter "Locked documents" +* New: #1118 - How can I add 'Comment' column in the file list view? +* Bug: #880 - Removing steps of approval_workflows causes data corruption + +3.0.6 *2022-09-20* +------------------ + + Default query + GitHub CI + PosgrSQL compatibility + +* Bug: #1401 - Duplicated steps in "New step" form +* Bug: #1399 - Open 'watched' folder throw internal error +* Bug: #1397 - System folder is not deleted +* New: #1396 - Repeatable rake task to sync documents +* Bug: #1395 - Use custom fields in filter throw error 500 +* New: #1386 - Changing default file query + +3.0.5 *2022-08-20* +------------------ + + Aproval wokflows notifications fix + +* Bug: #1394 - Email notifications for workflows are not sent + +3.0.4 *2022-08-19* +------------------- + + Version macro extension + +* Bug: #1392 - Issue #1388 patch +* Bug: #1391 - Fix plugin name redmine_checklists +* New: #1390 - Version of revision in wiki +* Bug: #1388 - Custom field in DMS Columns +* Bug: #1387 - Error in bundle with plugin custom table +* Bug: #1385 - Wrong version when uploading a document via WebDAV +* Bug: #1384 - Checksum is always the same via WebDAV + +3.0.3 *2022-07-19* +------------------- + + Security enhancement + Persian localisation + DMSF images in PDF export + +* Bug: #1382 - Unable to copy or move files +* Bug: #1381 - Update fa.yml +* Bug: #1380 - Custom queries and Trash bin +* New: #1377 - Create fa.yml +* New: #1375 - Hide the link in the TOP menu +* Bug: #1374 - Possible XSS Vulnerability by using eval() +* Bug: #1373 - Cross-site Scripting risk in Select2 < 4.0.8 +* Bug: #1372 - Replacing view_dmsf_file_path references with view_dmsf_file_url +* Bug: #1371 - Mail rendering of DMSF file link reports undefined method error +* Bug: #1369 - Translate settings column names +* Bug: #1368 - Wrong translation in Persian and probably some other languages +* New: #1082 - More than one ID in image rendering macros +* Bug: #903 - Little bug in PDF image export (Redmine 3.4.6) + +3.0.2 *2022-06-17* +------------------- + + MS SQL compatibility + RedmineUp's plugins compatibility + +* Bug: #1366 - 404 Not found while restoring documents from the trash bin +* Bug: #1365 - No journal when delete / de-attach document +* Bug: #1364 - Error while loading /settings +* Bug: #1363 - Conflict with plugin redmine_issue_evm +* Bug: #1352 - Error while loading list DMSF + +3.0.1 *2022-06-03* +------------------- + + Inline displaying of office documents + Custom fields displayed by folders + Compatibility with RedmineUp's plugins + Compatibility with Issue EVM plugin + New hooks + +* Bug: #1363 - Conflict with plugin redmine_issue_evm +* New: #1361 - Progress bar modal when handling document upload +* New: #1360 - Use Redmine's temp folder +* New: #1359 - Add download icon +* New: #1358 - Remove the magnifier icon +* Bug: #1357 - Check convert available by thumbnails +* Bug: #1356 - Remove a drive letter when using WebDAV in Windows +* New: #1355 - Hook Request +* Bug: #1354 - Redmine Configuration Page not working when DMSF is installed together with RedmineUp resources plugin +* New: #1353 - Cannot preview my doc +* Bug: #1350 - Internal Error while opening settings +* Bug: #1349 - QueryColumn - Error +* New: #1348 - Custom Fields not shown on folder level +* Bug: #1345 - Conflict with RedmineUP invoice plugin +* New: #1227 - Check if a document contains a signature +* New: #1203 - Suggest to add document preview + +3.0.0 *2022-04-28* +------------------- + + Redmine 5.0 + Watchable documents and folders (The original DMS notifications ar off as default. They can be activated in Administration->Settings->Email notifications.) + Patch version + +* New: #1344 - Need support Redmine 5 +* Bug: #1343 - New content input field improvement +* Bug: #1340 - Using plugin with Redmine SVN trunk +* Bug: #1339 - Not show action menu to unlock folder +* Bug: #1338 - move/copy folder from project1 to project2 +* New: #1337 - Project's plus menu extension +* New: #1336 - New file menu item +* New: #1333 - Memory problem in 'My page' with 'Open aprovals' option +* Bug: #1330 - Trouble with dmsff macro +* Bug: #1329 - Problem moving a folder with locked files. +* New: #1328 - Update de.yml +* New: #1323 - Fast links for Copy/Move +* Bug: #1318 - easy_gantt compatibility +* Bug: #1317 - Wrong links to a project +* Bug: #1314 - Fix HTTP Status 500 when emailing document link +* Bug: #1313 - Impossible to use macro in the revision comment field. +* New: #1312 - Update _log.html.erb +* Bug: #1311 - Deleting the link between files and issues +* New: #1151 - Add document revision.patch_version +* New: #557 - Watch Documents + +2.4.11 *2021-11-03* +------------------- + + GitLab CI + REST API + Copy and move of documents and folders + PostreSQL fixed + +* New: #1309 - Gitlab CI +* Bug: #1306 - Mysql2::Error: Operand should contain 1 column(s) +* Bug: #1304 - SQL error with postgresql on top menu +* New: #1301 - REST API for documents movement + +2.4.10 *2021-10-20* +------------------- + + German and English localisation improvement + +* Bug: #1299 - Added missing phrases in German translation, corrected typos (DE, EN) + +2.4.9 *2021-11-15* +------------------ + + Project copying DMSF options fixed. + Dalli dependency removed. + +* Bug: #1297 - Redmine KO after dalli upgrade to 3.0 +* Bug: #1296 - "Copy folders only" doesn't work properly + +2.4.8 *2021-10-08* +------------------ + + REST API + Create a revision, updating custom fields + Bug fixes + +* Bug: #1290 - Column is not shown. GUI is not changed +* Bug: #1284 - 500 Error when doing Approval Workflow related actions #1260 +* Bug: #1283 - Rest API: link file to an issue +* Bug: #1282 - Current view changed after adding new revision +* Bug: #1280 - Problem with editing an exisiting link +* Bug: #1279 - Error when sorting on custom field +* Bug: #1277 - Custom field not set as default column +* Bug: #1272 - Document tagging/filtering: filter not working when tag has multiple values +* Bug: #1267 - Document editing from a mounted folder in MS Excel 2016 +* Bug: #1265 - Cannot unlock a folder despite :force_file_unlock permission +* Bug: #1262 - Redmine version dependency differs between readme.md and init.rb +* Bug: #1260 - 500 Error when doing Approval Workflow related actions +* Bug: #1258 - Unlock folder results in 403 +* Bug: #1255 - Sub Folder creation via REST API +* Bug: #1254 - SQL Error when trying to list certain folders +* Bug: #1252 - WebDAV error +* New: #1245 - Update custom fields of a file with REST API + +2.4.7 *2021-05-12* +------------------ + + Bug fixes + +* Bug: #1251 - DMSF Security Vulnerability +* Bug: #1249 - Bug Version field interpreting int as BigDecimal + +2.4.6 *2021-04-30* +------------------ + + Global DMS view + Sub-projects as sub-folders + Redmine 4.2 + +* Bug: #1243 - Fixes modification during iteration +* New: #1241 - Get File by API with Custom Fields +* Bug: #1238 - Ubuntu 20.04 dependencies install problem +* New: #1232 - Allow approval workflow actions only on x.0 file versions +* Bug: #1231 - API doesn't respond like described in the docs +* New: #1230 - Redmine 4.2.0 support +* Bug: #1229 - Error when sorting by a custom field +* Bug: #1221 - WebDAV links to non top-level directories are broken +* New: #1217 - Global DMS view +* Bug: #1215 - DMS Documents > New file does not respect theme styles properly +* Bug: #1214 - Error 500 when create new revision +* Bug: #1213 - DMS project preferences do not save +* New: #1211 - Highlight admin menu item and very little fix of ProjectPatch#copy +* New: #1209 - Added some translations // Fixed typo +* New: #1207 - Sub-projects as sub-folders +* New: #1206 - Support for .xlsm files in Edit content +* New: #1204 - Empty trash bin function is missing +* New: #1201 - Givable roles for folder's permissions +* New: #1200 - Improvement of german translations +* New: #1199 - Breakdown structure of folders if a filter is set +* Bug: #1198 - 500 Internal error when press the DMS Modules +* New: #1196 - Add workflow step name to mail notification +* New: #1195 - Move to the bottom button +* Bug: #1194 - Cannot delete a link if the linked object is locked + +2.4.5 *2020-11-10* +------------------ + + Sub-projects as sub-folders in WebDAV + Delete and restore functions for non-empty folders + A new wiki macro for embedded videos + +* Bug: #1184 - Problems uploading files with the same file name as attachments on Redmine issues +* Bug: #1183 - Update README.md +* Bug: #1179 - Can not make file or folder which have the same name as the project's root folder, and etc. +* New: #1178 - Failed to PUT files which includes some characters via WebDAV +* Bug: #1175 - Available in CSV Internal Error +* Bug: #1172 - Manually locking document disables "Edit content" +* Bug: #1170 - Max size of upload-able file +* Bug: #1166 - Version column in documents table can't display letters +* Bug: #1165 - DMSF 2.4.4 1 byte files issue +* New: #1164 - Embed video into wiki +* Bug: #1163 - Folder visible via webdav but not via UI +* Bug: #1159 - Approval workflow log not available for non-admin users +* Bug: #1156 - Editing a document also changes its title +* Bug: #1155 - Bug easy context menu +* Bug: #1150 - Uploading big files causes no memory exception +* New: #1145 - Folder can not be deleted if the folder contains files or folders +* New: #1136 - WebDAV tree structure including sub-projects duplicate +* New: #1023 - New UI: List view improvements +* New: #1122 - New UI: Custom fields as filters +* Bug: #1088 - Webdav link contains SUB-URI part twice +* New: #460 - Webdav: Parent-sub Project Folders Seperated + +2.4.4 *2020-07-10* +------------------ + + Maintenance release + +* New: #1144 - Who has locked the document information is missing. +* Bug: #1142 - How to configure "Direct document or document link sending via email"? + +2.4.3 *2020-06-12* +------------------ + + Redmine's look&feel + Implementation of folders movement between projects (WebDAV) + Korean localization updated + + +* New: #1129 - New UI: Optimize Actions Menu +* New: #1128 - New German translations +* New: #1127 - Help integrating new feature - Auto-update word files with dmsf revision +* Bug: #1125 - New UI: Question concerning the new filtering options +* Bug: #1121 - New UI: Saving Query -> Internal Server Error +* Bug: #1120 - New UI: Values of custom fields not visible +* New: #1119 - Button "New folder" maybe must be replaced nearly button "New file" (UI better solution) +* New: #1115 - Ruby 2.3 compatibility +* New: #1112 - Update ru.yml +* Bug: #1110 - Error redmine 4.1.1 after devel-2.4.3 dmsf upgrade +* Bug: #1106 - Status 404 after moving the folder to another project +* New: #1100 - Update Korean translation +* Bug: #1095 - Public URL date cannot be set in Chromium based browsers +* New: #1084 - Update Korean translation +* New: #1080 - Redmine look and feel +* Bug: #1075 - DMSF main page not opening for a few users (Error 500) +* New: #236 - Documents tagging +* New: #29 - Improve/AJAXify DMSF browsing UI + +2.4.2 *2020-01-21* +------------------ + + Compatibility with Redmine 4.1 + Chinese localisation updated + +* New: #1072 - Bug deprecation multiple gemfile sources +* New: #1069 - Minor version is limited to 99 max - I recommend to change the limit to 999 +* New: #1068 - [travis] test redmine 4.1.0 +* New: #1067 - update redmine extensions +* New: #1066 - Create zh-TW.json +* Bug: #1065 - Installation error version 2.4.1 +* Bug: #1064 - Wrong sorting of Czech characters +* Bug: #1060 - XSS fix +* Bug: #1058 - DMS form flickering on page reload +* New: #1055 - Autofill of folder link name +* New: #1054 - Displaying of inherited permission in Folder permissions +* Bug: #1052 - Accessible even if WebDAV is disabled +* Bug: #1051 - redmine:dmsf_alert_approvals rake task on closed projects +* Bug: #1046 - Redirect to parent folder after folder edit +* Bug: #1041 - Download button gets disabled after first download +* Bug: #1038 - Webdav not open file +* Bug: #932 - Undefined method 'to_prepare' for ActionDispatch::Reloader:Class (Redmine 4.0 / Rails 5) +* Bug: #913 - ActionController::RoutingError (No route matches [PROPFIND] "/") +* New: #908 - Wrapping problem in Issue view + +2.4.1 *2019-09-13* +------------------ + + Compatibility with Redmine 4.0.4 + Japanese localization updated + Plupload & DataTables libraries upgraded + +* Bug: #1033 - Bitnami Redmine 4.0.4 +* New: #1032 - Deprecate silverlight support? +* New: #1023 - Project menu is not displayed in Redmine 4.0.3 +* Bug: #1019 - Internal Erro 500 when enable "Act as attachable" and access Activity page +* Bug: #1017 - Multiple zip files are filling the tmp folder +* Bug: #1015 - WebDAV client error +* Bug: #1013 - Approval workflow notifications are sent to locked users +* Bug: #1010 - Installing Redmine in a sub URI +* Bug: #1008 - Description field trunkates on blank line +* Bug: #1004 - Wrong revision order after upgrading to DMSF 1.6.2 +* Bug: #1003 - Wrong file structure on migrate +* Bug: #1002 - New folder with empty titlle => Error 500 +* Bug: #1001 - User Permission problem (can't choose user) +* Bug: #995 - All files and folders deleted during migration +* Bug: #992 - No such file to load -- mime/types.rb (LoadError) +* Bug: #988 - Failure to Update DMSF from 1.5.9 to 2.0.0 during migrate +* New: #987 - Update Japanese translation +* Bug: #986 - I can not send file by mail +* Bug: #984 - Uninitialized constant Redmine::IntegrationTest NameError +* Bug: #980 - Copy of root folder to subfolder causes web crash +* Bug: #932 - Undefined method `to_prepare' for ActionDispatch::Reloader:Class (Redmine 4.0 / Rails 5) +* Bug: #918 - Some local json file doesn't load +* Bug: #905 - Custom Fields of type 'url' are displayed "as plain text" in document listing. + +2.0.0 *2019-02-28* +------------------ + + Compatibility with Redmine 4.0 + Russian localization updated + +* Bug: #976 - Can't link document to issue with column in subject +* Bug: #969 - About the DMSF folder search logic +* Bug: #966 - folder_manipulation permission +* Bug: #965 - tag column missing in the dms_file_revision_table +* Bug: #959 - crete symbolic link error +* Bug: #956 - About "External" of "Link from" +* Bug: #950 - Wrong description, missing argument for macro {{dmsft}} +* Bug: #940 - dav4rack license +* Bug: #937 - Documents upload if disk is full +* Bug: #936 - Then go to configuration an internal error #500 appear +* Bug: #935 - Upload failure for 2.0 +* Bug: #934 - problem to get reversion error +* Bug: #933 - změny v xapian_indexer +* Bug: #932 - undefined method `to_prepare' for ActionDispatch::Reloader:Class (Redmine 4.0 / Rails 5) +* Bug: #929 - Problems in revision history +* New: #928 - About Redmine 4.0.0 +* New: #576 - Installation problem 1.5.7 (step 4 of the guide) + +1.6.2 *2018-12-04* +------------------ + + REST API + doc/folder deletion + doc's title property added + creating links + limit & offset parameters added for pagination + Speed up + Fast links option + Folder edit's form + Approval workflow + Obsolete state added + +* Bug: #907 - label_webdav is duplicated in local files +* New: #887 - REST API 'Get document' : 'title' property is missing in response +* Bug: #885 - Open Remote in LibreOffice +* Bug: #881 - DMSF access for anonymous users +* New: #878 - Enlarge "Link To" form fields +* Bug: #867 - Attached documents remain by issues after they had been deleted in the main Document view +* Bug: #866 - A problem by attaching documents to issues +* New: #857 - Xapian not indexing repository if project configuration is blank +* New: #855 - Workflow notification missing +* New: #852 - Create symbolic link using REST API +* New: #850 - REST API and pagination on collection resources +* New: #847 - REST API and delete Folder/document +* New: #823 - Office URI Scheme for direct editing of MS Office files +* Bug: #818 - Xapian not available +* New: #803 - 'Create folder' takes a very long time +* New: #798 - Possibility of Obsolete an Approved Version of a Document + +1.6.1 *2018-04-03* +------------------ + + Javascript on pages is loaded asynchronously + Obsolete Dav4Rack gem replaced with an up to date fork by Planio (Consequently WebDAV caching has been removed, sorry...) + Cloned from gem https://github.com/planio-gmbh/dav4rack.git + Project members can be chosen as recipients when sending documents by email + Responsive view (optimized for mobile devices) + Direct editing of document in MS Office + Korean & Dutch localisation + Move folder feature + Document versions can contain letters + +IMPORTANT + +1. `alias_method_chain` has been replaced with `prepend`. Not directly but using `RedmineExtensions::PatchManager`. + Consequently, there might occure conficts with plugins which overwrite the same methods. + +* Bug: #839 - Webdav not working +* New: #838 - Rake task for regenerating document's digests +* Bug: #831 - ActionView::Template::Error, when i am creating issue from the list of all projects +* Bug: #830 - ActiveRecord::StatementInvalid: Mysql2::Error: Table 'dmsf_file_revisions' doesn't exist +* Bug: #827 - Can't see files via WebDav, but see them via web-portal +* New: #823 - Office URI Scheme for direct editing of MS Office files +* New: #821 - Security Issue (Mail-Spoofing) +* Bug: #817 - The check for approval is not displayed +* Bug: #812 - Moving Issue to other project does not move attached documents in DMS +* Bug: #807 - alias_method_chain is deprecated +* Bug: #805 - Missing access check in search results +* New: #804 - Move folder +* New: #803 - 'Create folder' takes a very long time +* New: #802 - def log_activity functions +* New: #801 - Editing user in an approval workflow step +* New: #793 - Support for letters as the major version +* New: #790 - Confusing search options titles +* Bug: #789 - Mail Notification for deletion missing +* Bug: #784 - Document digist is not calculated when uploading via WebDAV +* New: #783 - Redmine uses SHA256 instead of MD5 for file digests +* New: #736 - Setting tmp folder path via ENV +* New: #726 - Keep the modification date of the file +* Bug: #716 - Microsoft Office webdav save throwing some UTF8 filename problems +* Bug: #708 - Configuration of the email sending form +* Bug: #687 - Not sorting correctly by the column title +* New: #682 - Responsive view +* New: #637 - Case of blank filetitle with revision id +* New: #628 - To rename members.title_format to members.dmsf_title_format +* Bug: #616 - An attempt to create a folder in the root causes an infinite loop +* New: #492 - Does the redmine_dmsf support to choose the members of current project when email? +* New: #231 - Better referencing macro + +1.6.0 *2017-09-12* +------------------ + + Folder permissions + Documents attachable to issues + Hungarian localization + Full-text search in *.eml and *.msg + +IMPORTANT + +1. Files in the filesystem are re-organized by a new system based on dates. So, documents are not stored in folders named + by the project's identifier but by the data of uploading, e.g. 2017/09. It's the same system used by Redmine for + attachments. +2. DMS storage directory plugin option is related to the rails root directory. +3. The plugin is independent of the gem xapian-full-alaveteli which has been replaced with ruby-xapian package. Therefore + is recommended to uninstall xapian-full-alaveteli gem and install ruby-xapian package in order the full-text search + is working. + +* Bug: #758 - Error in template when retrieving details of a file in a subfolder +* New: #755 - Ability to retrieve the MD5 value of a Document type +* Bug: #749 - REST API - List of documents in folder fails when using folder_title +* Bug: #747 - Background icon repeating in admin panel (Redmine 3.4.2) +* Bug: #746 - Thumbnail macro: size paramter not respected +* Bug: #744 - Full stops within filename lead to false extensions +* New: #742 - WebDAV PROPSTATS and PROPFIND caching change +* Bug: #738 - Upload failure +* Bug: #734 - DMSF uploader seems to override built in uploader +* New: #733 - Make the storage path Rails.root related +* Bug: #732 - Buggy tree view +* Bug: #731 - Add users for new step in Worflow Dialogue +* Bug: #730 - Workflow "New Step" dialog not appearing +* Bug: #728 - Internal error 500 when uploading document via Edit issue +* New: #727 - Ability to disable document upload in issues +* Bug: #725 - Can't uninstall redmine dmsf in Bitnami +* New: #717 - Enhacement: Xapian parse eml and msg files in same way as word, excel... +* Bug: #714 - The full text search does not work +* New: #713 - Hungarian localisation +* New: #712 - Notifications ON/OFF are confusing +* Bug: #710 - Can't delete locked documents from the trash +* Bug: #701 - How tagging with multiple values works? +* Bug: #700 - 'Save as' from Excel does not work when using project names +* New: #699 - Speed up the main view +* New: #697 - Email notifications from WebDAV interface +* Bug: #694 - redmine:dmsf_convert_documents +* Bug: #693 - redmine:dmsf_convert_documents +* Bug: #692 - Error migrate plugin v1.5.9 +* New: #691 - The last approver in the CSV export +* Bug: #685 - Problem deleting plugin +* Bug: #683 - Approval reminder problem +* New: #667 - A better navigation in found results +* New: #651 - Incomplete copy of a file to another project +* Bug: #623 - Option "Navigate folders in a tree" seems not to be saved +* New: #543 - Feature Request: Document Location - Folder Structure +* New: #170 - Document and Folder Access Control. This issue may be duplicated as I saw it on google code some time ago. +* New: #48 - Linking Issues and DMSF Documents + +1.5.9 *2016-03-01* +------------------ + + WebDAV + Documents editing in MS Office + Support for rsync and cp commands + Disable verioning for certain file names pattern by PUT request + Ignoring certain file names pattern by PUT request + Caching of PROPSTATS and PROPFIND requests + REST API + Update folders + Finding folders by their titles + Approval workflow + Editing of approval workflow steps + Approval workflow step name + DMSF + Document export + Public URLs option in email entries + Global title format for downloading + New columns in the main DMSF view; columns are configurable from the plugin settings + +* New: #676 - An option to prevent inheritance of CF +* New: #675 - Keep documents locked after the approval workflow is finished as an option +* Bug: #671 - Webdav: MOVE returns incorrect response +* Bug: #663 - Locked documnts on My page +* Bug: #662 - Broken paging on the Add approver form +* New: #655 - ERROR: Couldn't find Project with identifier=desktop.ini +* New: #654 - Non-versioned files should not go to trash bin when deleted +* Bug: #652 - Missing date picker when creating new file +* Bug: #651 - Incomplete copy of a file to another project +* New: #648 - Lock duration +* New: #641 - Documents export +* New: #635 - Edit approval workflow steps +* Bug: #632 - database migration error (from ver 0.9.1 to ver 1.5.8) +* New: #630 - Disable versioning for certain files/file patterns +* New: #629 - Approval workflow step name +* New: #626 - Public URLs in email entries +* New: #614 - WebDAV caching +* Bug: #606 - DmsfFile.move_to does not update last_revision +* Bug: #605 - Wrong file size detection for non English language +* Bug: #603 - Send documents by email, from address is emission email address instead of user mail +* Bug: #598 - WebDAV: PROPFIND to "/" and "/dmsf" throws FATAL error +* Bug: #593 - Modern upload file type doesn't work +* Bug: #592 - reset_column_information is missing in DB migration +* Bug: #591 - rsync doesn't work for WebDAV mounted folder +* Bug: #587 - Working with MS Office documents directly in mounted WebDAV share +* New: #584 - A lot of warnings in WebDAV unit tests +* Bug: #582 - FATAL -- : ActionController::RoutingError (No route matches [GET] "/plugin_assets/redmine_dmsf/javascripts/jquery.dataTables/zh.json") +* Bug: #581 - Webdav always shows the create date +* Bug: #580 - Revision deleting +* Bug: #579 - Wrong file size +* New: #555 - Documents ID easy access +* New: #551 - Default action for files viewing +* New: #547 - Setting Title format should be global setting, but released as local setting +* New: #499 - Add column "type/extension" in folder content view + +1.5.8 *2016-10-21* +------------------ + + Drag&Drop for a new content in the new revision form + Tree view optimization for speed + Wiki macros revision: dmsfd X dmsfdesc + Support for deleting users + +* Bug: #578 - A wrong title when uploading documents +* Bug: #574 - Macro {{dmsfd(xx)}} produce blank value +* Bug: #566 - HTML tags in the document description breaks UI +* Bug: #565 - Error 500 when a link to another folder is in the folder/project +* New: #562 - New step button text +* Bug: #561 - Wrong path in the document's details form +* Bug: #560 - Trying to send mail without recipient results in error 500 +* Bug: #558 - Deletion of a user +* New: #443 - Drag/drop feature for new content + +1.5.7 *2016-08-12* +------------------ + + SQLite compatibility + Lock/Unlock feature for global approval workflows + Document ID in the document's details + New wiki macros (thumbnail, approval workflow) + Searchable pick lists + Tree view as an user's option + Italian localisation + +* Bug: #556 - Plugin settings "File default notifications" does not apply! +* Bug: #554 - JQeury datatable not load correct language file +* Bug: #545 - Wrong tool-tip for dmsf macro +* Bug: #544 - Approval workflow email notifications +* Bug: #542 - Link from commbo box sorting +* Bug: #538 - Migration error with Redmine 3.3 +* New: #532 - Modified timestamps lost after migration +* Bug: #531 - webdav: Error -36 on OSX +* Bug: #530 - Cannot download folders with sub folders +* New: #529 - Show document description in mouseover or column +* New: #527 - Add MD5 of each revision in the detail view of documents +* Bug: #526 - The same version feature doen't work as expected +* Bug: #523 - Bug with "delegate approval step" +* Bug: #522 - File Storage Directory does not change +* New: #520 - Document link after a search ... +* New: #518 - Debian installation issues +* Bug: #506 - Document title format %t doesn't reffer to the title +* Bug: #504 - Non-fatal MySQL error when migrating documents +* New: #503 - Information about migrating documents +* Bug: #501 - If a folder or file is locked, we can't activate or deactivate notifications +* New: #500 - Automatically check the inline radiobutton when use custom version +* New: #252 - nautilus-like folders-files list view + + +1.5.6 *2016-01-25* +------------------ + + Uploading of large files (>2GB) + Support for *.svg and *.py in wiki macros + File name formatting while downloading + +* Bug: #498 - Webdav: Invalid handling of files with '[' or ']' in file name +* New: #497 - file.image ignore SVG type +* Bug: #494 - Unable to upload files with ruby > ruby-2.0.0-p598 +* Bug: #491 - Still using original uploaded filename after filename renamed (PDF file) +* Bug: #488 - Available projects for 'link to' operation +* Bug: #487 - Not able to view the url link file, but able to Download +* Bug: #480 - Big files ( > 500mb) uploading problems +* Bug: #471 - Converting Documents to DMSF is not working +* Bug: #470 - sort function +* Bug: #469 - dmsfd doesn't reuse Wiki syntax in Wiki page +* New: #468 - Display contents of text file in Wiki page +* Bug: #465 - Install using debian 8 (jessie) +* Bug: #459 - WebDav Windows +* Bug: #458 - Cannot upload big files +* New: #44 - Append File Revision on filename when downloading file + + Maintenance release II. + +1.5.5 *2015-10-19* +------------------ + + Maintenance release + +* Bug: #457 - Folder name Documents inaccessible +* Bug: #456 - Everything is set to DEACTIVATED but still I got notifications +* Bug: #448 - C:\fakepath\ added to Revision Filename Path +* Bug: #432 - approval process for 3 users "user1 AND user2 AND user3" +* Bug: #109 - Rename folder over webdav + +1.5.4 *2015-09-17* +------------------ + + New DMSF macro for inline pictures + File name updating when a new content is uploaded + System files filtering when working with WebDAV + +* Bug: #442 - Can't move directories +* Bug: #441 - Drag'n'drop save only the picture thumbnail +* New: #438 - The file name update when a new content is uploaded +* Bug: #418 - Documents details from in IE +* Bug: #417 - Selected column is not highlighted under mouse pointer in IE +* New: #352 - DMSF Macro to display inline Pictures in wiki +* New: #54 - Webdav: Filter Mac OS X "resource forks" files + +1.5.3 *2015-08-10* +------------------ + + Plupload 2.1.8 + Redmine >= 3.0 required + +* Bug: #430 - Got 500 error when change directory name +* Bug: #427 - Can't access to WebDAV root directory +* Bug: #422 - Document uploading in IE + +1.5.2 *2015-07-13* +------------------ + + Redmine >= 3.0 compatibility + +* Bug: #404 - Deleted folder (still in trash) results in errors while accessing parent folder via webdav +* Bug: #401 - Link between project on Redmine 3.0 +* Bug: #400 - internal server error fulltext search +* Bug: #396 - Error when uploading files +* Bug: #394 - DMSF install to Redmine 3.0.3 problem +* Bug: #393 - File can't be created in storage directory (Redmine 3.0.3) +* Bug: #392 - Redmine 3 Search screen error with Xapian +* New: #391 - Searchable document details +* Bug: #387 - Wrong sorting by Modified column +* Bug: #384 - Error when trying to uninstall DMSF +* New: #383 - Missleading number of entities in documents folder +* Bug: #382 - REST API - list of document produces invalid XML +* Bug: #380 - Internal Error 500 when dmsf page is accessed +* Bug: #378 - Revision view, delete revision bug +* Bug: #377 - Can access WebDAV when redmine is located under sub-URI +* Bug: #376 - Links to deleted documents +* Bug: #374 - Number of downloads +* Bug: #373 - internal 500 error : 1.5.1 stable with redmine 3.0.1 when search in dmsf enabled project +* New: #339 - Maximum Upload Size +* Bug: #319 - webdav problem after upgrading to 1.4.9 from 1.4.6 +* New: #78 - Control DMSF via REST API + +1.5.1: *2015-04-01* +------------------- + + Approval workflow copying + Polish localization + Custom versions for new document revisions + External links + +* New: #307 - Filter mail receivers for approval workflow with file managing permission +* New: #308 - Rails 4 +* Bug: #321 - My open approvals +* Bug: #322 - Approval workflow notifications +* New: #325 - Approval workflow permission +* New: #326 - Approval workflow copying +* Bug: #327 - ArgumentError: Unknown key: :conditions. (when running migration in redmine 2.6) +* Bug: #330 - File link cannot download/email +* New: #332 - ArgumentError: Unknown key: :conditions. (when running migration in redmine 2.6) +* Bug: #323 - NoMethodError (undefined method `major_version' for nil:NilClass) +* Bug: #336 - Delete documents configuration works for all the roles +* Bug: #340 - Unwanted notifications +* Bug: #341 - Error on approval workflow +* Bug: #343 - Can't use a name of a folder already existing in the trash bin +* Bug: #350 - Link seems wrong in when clicking "Approval workflow name" +* New: #351 - [Feature Request] - overriding preconfigured Revision Tags/Steps +* Bug: #353 - Link to User in Doc-Revision seems to point to wrong target link +* New: #357 - Redmine 3.0.0 released! Compatibility with DMSF? +* Bug: #361 - incompatible encoding regexp match (UTF-8 regexp with ASCII-8BIT string) +* Bug: #366 - unable to properly uninstall under Redmine 3.0.1 +* Bug: #367 - Unable to create a folder +* Bug: #368 - Cannot create a document workflow +* Bug: #369 - Update document revision under Redmine 3.0.1 +* Bug: #371 - Unable to properly uninstall the plugin +* Bug: #372 - Can't move file via WebDav + +1.4.9: *2014-10-17* +------------------- + + Trash bin + Standard Redmine's upload form with progress bar for files > 100 MB + WebDAV library upgrade + +* New: #130 - redmine_dmsf: last update of the folders +* Bug: #131 - Wiki link shows filename for all users type +* New: #136 - `File Manipulation` permissions +* New: #218 - Feature request: Recycle bin +* Bug: #226 - Undefined method `custom_fields_tabs` for module `CustomFieldsHelper` +* New: #238 - DMSF document update shows up in issue referred to in comment +* New: #249 - Storage path for DMSF files ignores global storage path for attachments +* New: #255 - Debian - Readme install procedure update +* Bug: #258 - Jquery conflict with Redmine +* Bug: #267 - Custom fields tabs not work with last custom_fields_helper_patch.rb +* Bug: #269 - Workflow OR not working for second reviewer +* Bug: #270 - 500 Internal Server Error, redmine 2.5.1, MS SQL Server 2012, dmsf 1.4.8-master, dmsf_link.rb +* Bug: #275 - Typo in readme file type +* Bug: #288 - ubuntu migrate failed +* Bug: #290 - error installing plugin +* Bug: #293 - Locking of inexistent files fails +* Bug: #298 - The same approver in one approval step +* Update: #301 - Database normalization + +1.4.8: *2014-04-17* +------------------- + + Symbolic links + Document tagging + Localization of email notifications + An option to send document links by email + +* New: #19 - Documentation? +* New: #106 - [Feature Request] Save files in folder structure defined via DMSF +* Bug: #107 - Problems upgrading redmine 1.3 to 2.23 regarding DMFS +* Bug: #111 - Cannot sort files in folders by date, size, etc +* Bug: #139 - Error 500 on click on "details" icon +* New: #183 - Create document links +* New: #201 - Download link by email +* Bug: #205 - Ampersand shows up in displayed filenames as "&" instead of "&" +* Bug: #212 - Incorrect revision information in email notification +* Bug: #214 - Required DMSF custom field prevents documents to be saved +* New: #216 - Enhancement : having notification emails translated +* New: #224 - Setup/Upgrade documentation +* Bug: #226 - undefined method `custom_fields_tabs` for module `CustomFieldsHelper` +* Bug: #233 - Failed Travis builds +* New: #235 - "You are not member of the project" when changing project notification. +* New: #236 - Documents tagging +* Bug: #240 - Internal server error, redmine 2.5.1-devel.13064, PostgreSQL, dmsf 1.4.8-devel +* Bug: #242 - dsmf 1.4.8 minor ... "link form" tab +* Bug: #246 - "File storage directory" does not default properly when setting is empty + +1.4.7: *2014-01-02* +------------------- + + Open approvals in My page + Custom fields + Speeding up + Code revision + +* New: #38 - A few questions about the plugin (possible improvements) +* New: #49 - Make the 100 MB ajax upload limit an option +* Bug: #52 - Error : undefined method `size' for nil:NilClass +* Bug: #90 - Missing redmine_dmsf / assets / javascripts / plupload / i18n /en.js file? +* Bug: #94 - Files not deleted with project +* Bug: #95 - DMSF tab missing on closed projects +* Bug: #104 - Custom fields do not work +* Bug: #141 - Error 500 uploading file with DMSF custom fields +* Bug: #159 - Broken links caused by plugin_asset_path implementation +* New: #173 - Open approvals in My page +* Bug: #174 - Workflow error when more than one approver +* Bug: #175 - Error 500 on performing search +* Bug: #176 - 500 internal error when approving workflow - dmsf_workflows/4/new_action +* Bug: #177 - 1.4.7-devel unable to upload files +* Bug: #178 - Error 500 cannot access Administration -> Custom Fields page +* New: #179 - Workflow Log History in Detailed View +* Bug: #187 - Approval workflow permissions +* New: #190 - Very slow in directories containing many files +* Bug: #191 - Move/Copy gives undefined method for File:Class +* New: #193 - French translation +* Bug: #194 - Workflow name link in workflow log window +* Bug: #195 - Workflow log not displaying all the steps +* New: #196 - Update French Language +* Bug: #197 - Multi upload not loading the translation +* New: #198 - When editing a workflow, only show current project's users +* Bug: #199 - Small error in plugin_asset_path function +* New: #200 - Update the french translation for the multi upload module +* Bug: #202 - unable to create Custom Field when DMSF plugin installed +* Bug: #203 - Little typing error in french translation +* Bug: #206 - "Select All" checkbox not functioning +* Bug: #207 - locks by deleted users producte internal server error 500 + +1.4.6: *2013-10-18* +------------------- + +* New: Document approval workflow +* New: Slovene language translation +* New: German language translation +* Bug: #34 - fix does not function as expected on Rails < 3.2.6, Redmine 2.0.3 dependency added. +* Bug: #75 - Wrong filename encoding in emailed zip file +* Bug: #87 - RoutingError (No route matches [GET] "/javascripts/jstoolbar/lang/jstoolbar-en-IS.js"): +* Bug: #103 - Multiple DMSF tabs in Administration->Custom fields & localization +* Bug: #110 - 'zip' gem conflicts with 'rubyzip' on Redmine XLS Export Plugin. +* Bug: #112 - Uninstall command +* Bug: #116 - Translation missing for DMSF custom field tabs +* Bug: #146 - Problem with Russian file names in zip +* Bug: #143 - Error on missing template - has to have to_s if adding to string +* Bug: #148 - I don't have a notification sent out when I upload a file +* Bug: #157 - Copying files/folders from one project to another project + +1.4.5: *2012-07-20* +------------------- + +* New: Settings introduced to enable read-only or read-write stance to be taken with webdav +* Bug: #27 - incorrect call to display column information from database (redmine 1.x fragment). +* Bug: #28 - incompatible SQL in db migration script for postgresql +* Bug: #23 - Incorrect call to to_s for displaying time in certain views +* Bug: #24 - Incorrect times shown on revision history / documents +* Bug: #25 - Character in init.rb stops execution +* Bug: #34 - Incorrect scope when accessing deleted files prevented notification. + +1.4.4p2: *2012-07-08* +--------------------- + +* Bug: #22 - Webdav upload with passenger/nginx fails with server error (passenger class for request.body does not contain length method. +* Bug: Additional check implemented before reading settings to prevent server error when setting is not set and default does not apply. + +1.4.4p1: *2012-07-07* +--------------------- + +* Bug: #20 - Listing not functional when using sqlite adapter +* Bug: #21 - Webdav not functional under bitnami (or sub directory) +* Bug: Testcase failed to cleanup after itself +* Bug: Webdav index object identified itself as having parent under prefix'ed path (in error) +* Bug: Addition of a path_prefix routine for webdav to be able to correct redirects + +1.4.4: *2012-07-01* +------------------- + +* New: Locking model updated to support shared and exclusive write locks. [At present UI and Webdav only support exclusive locking however] +* New: Folders are now write lockable (shared and exclusively) [UI upgraded to support folder locking, however only exclusively] +* New: Locks can now have a time-limit [Not yet supported from UI] +* New: Inereted lock support (locked folders child entries are now flagged as locked) +* Bug: Some testcases erroniously passed when files are locked, when they should be unlocked +* New: Webdav locks files for 1 hour at a time (requested time is ignored) +* New: Files are now stored in project relevent folder +* New: Implementation of lockdiscovery and supportedlock property requests +* New: Locks store a timestamp based UUID string enabling better interaction with webservices +* Bug: #16 - unable to add new project when plugin enabled due to bug in UI +* Bug: #17 - dav4rack not installable on some systems - it is now vendored +* Bug: #18 - Warnings thrown due to space between function and parentheses + +1.4.3: *2012-06-26* +------------------- + +* New: Hook into project copy functionality to permit (although not attractively) + functionality for DMSF to be duplicated accross projects +* New: Project patch defines linkage between DMSF files and DMSF folders. +* New: Data linkage allowing dependent items to be deleted (project deletion for example) + this needs to be revised as files marked deleted are not affected by this at present +* New: README.md updated with Bundler requirement (Issue #13) +* Bug: Error in entity details page UI prevented revision management. + +1.4.2: *2012-06-21* +------------------- + +* New: Integration test cases for webdav functionality +* New: Documentation has been converted from Simpletext to Markdown +* New: Features listed in documentation +* Bug: #3 - "webdav broken until set in Administrator -> Settings" +* Bug: #5 - "Webdav incorrectly provides empty listing for non-DMSF enabled projects" +* Bug: Issues identified by test cases + +1.4.1: *2012-06-15* +------------------- + +* New: Dav4rack requirement added (Gemfile makes reference to github repository for latest release). +* New: Webdav functionality included, additional administrative settings added +* Bug: #2 - extended xapian search fixed with Rails 3 compatible code. + +1.4.0: *2012-06-06* +------------------- + +* New: Redmine 2.0 or higher is required diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..5329c60e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at karel.picman@kontron.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..30c47144 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +## How to contribute to Redmine DMSF plugin + +#### **Did you find a bug?** + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/danmunn/redmine_dmsf/issues). +* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/danmunn/redmine_dmsf/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, **Redmine and the plugin version**, a part of the **production.log** related to the issue and **screen-shots**. + +#### **A wrong or missing translation?** + +* Feel free to update language files (_config/locales/*.yml_) in your mother language or languages you are familiar with and send them back to us via a **pull request**. + +#### **Do you want to patch the existing code?** + +* Make a new fork of the current development branch (_devel_). Make your changes and [create a pull request](https://github.com/danmunn/redmine_dmsf/compare). Only pull requests into the **development branch** except those fixing serious errors will be accepted. diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..f795a06a --- /dev/null +++ b/Gemfile @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +source 'https://rubygems.org' do + gem 'ox' # Dav4Rack + gem 'rake' unless Dir.exist?(File.expand_path('../../redmine_dashboard', __FILE__)) + 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 + unless %w[easy_gantt custom_tables] + .any? { |plugin| Dir.exist?(File.expand_path("../../#{plugin}", __FILE__)) } + group :test do + gem 'rails-controller-testing' + end + end +end diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..e21a50be --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,617 @@ +# GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom +to share and change all versions of a program--to make sure it remains +free software for all its users. We, the Free Software Foundation, use +the GNU General Public License for most of our software; it applies +also to any other work released this way by its authors. You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you +have certain responsibilities if you distribute copies of the +software, or if you modify it: responsibilities to respect the freedom +of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the +manufacturer can do so. This is fundamentally incompatible with the +aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for +individuals to use, which is precisely where it is most unacceptable. +Therefore, we have designed this version of the GPL to prohibit the +practice for those products. If such problems arise substantially in +other domains, we stand ready to extend this provision to those +domains in future versions of the GPL, as needed to protect the +freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish +to avoid the special danger that patents applied to a free program +could make it effectively proprietary. To prevent this, the GPL +assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in +detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU General Public +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that numbered version or +of any later version published by the Free Software Foundation. If the +Program does not specify a version number of the GNU General Public +License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU General Public License can be used, that proxy's public +statement of acceptance of a version permanently authorizes you to +choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..bfef3a14 --- /dev/null +++ b/README.md @@ -0,0 +1,339 @@ +# Redmine DMSF Plugin 4.2.3 + +[![GitHub CI](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml/badge.svg?branch=master)](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml) +[![Support Ukraine Badge](https://bit.ly/support-ukraine-now)](https://github.com/support-ukraine/support-ukraine) + +Redmine DMSF is Document Management System Features plugin for Redmine issue tracking system; It is aimed to replace current Redmine's Documents module. + +Redmine DMSF now comes bundled with WebDAV functionality: if switched on within plugin settings this will be accessible from _/dmsf/webdav_. + +WebDAV functionality is provided through Dav4Rack library. + +The development has been supported by [Kontron](https://www.kontron.com) and has been released as open source thanks to their generosity. +Project home: + +Redmine Document Management System "Features" plugin is distributed under GNU General Public License v3 (GPL). +33Redmine is a flexible project management web application, released under the terms of the GNU General Public License v2 (GPL) at + +Further information about the GPL license can be found at + + +## 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. Initialize/Update database: + + `RAILS_ENV=production bundle exec rake redmine:plugins:migrate NAME=redmine_dmsf` + +6. Install assets + + `RAILS_ENV="production" bundle exec rake assets:precompile` + +7. The access rights must be set for web server, e.g.: + + `chown -R www-data:www-data plugins/redmine_dmsf` + +8. Restart the web server, e.g.: + + `systemctl restart apache2` + +9. 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.) +10. Don't forget to grant permissions for DMSF in Administration -> Roles and permissions +11. Assign DMSF permissions to appropriate roles. +12. There are a few 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 yor `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 +``` + +### 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 JetBrains logo for providing an excellent IDE. diff --git a/app/controllers/dmsf_context_menus_controller.rb b/app/controllers/dmsf_context_menus_controller.rb new file mode 100644 index 00000000..0b76886c --- /dev/null +++ b/app/controllers/dmsf_context_menus_controller.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/preview" + +# Context menu controller +class DmsfContextMenusController < ApplicationController + helper :context_menus + helper :watchers + helper :dmsf + + before_action :find_folder + before_action :find_dmsf_file + before_action :find_dmsf_folder + before_action :find_dmsf_project + + def dmsf + if @dmsf_file + @locked = @dmsf_file.locked? + @project = @dmsf_file.project + @allowed = User.current.allowed_to? :file_manipulation, @project + @unlockable = @allowed && @dmsf_file.unlockable? && (!@dmsf_file.locked_for_user? || + User.current.allowed_to?(:force_file_unlock, @project)) + @email_allowed = User.current.allowed_to?(:email_documents, @project) + @preview = RedmineDmsf.office_bin.present? && RedmineDmsf::Preview.office_available? + elsif @dmsf_folder + @locked = @dmsf_folder.locked? + @project = @dmsf_folder.project + @allowed = User.current.allowed_to?(:folder_manipulation, @project) + @unlockable = @allowed && @dmsf_folder.unlockable? && (!@dmsf_folder.locked_for_user? || + User.current.allowed_to?(:force_file_unlock, @project)) + @email_allowed = User.current.allowed_to?(:email_documents, @project) + elsif @dmsf_link # url link + @locked = false + @unlockable = false + @project = @dmsf_link.project + @allowed = User.current.allowed_to? :file_manipulation, @project + @email_allowed = false + elsif @dmsf_project # project + @locked = false + @unlockable = false + @project = @dmsf_project + @allowed = User.current.allowed_to? :view_project_watchers, @project + @email_allowed = false + else # multiple selection + @project = project + @locked = false + @unlockable = false + @allowed = User.current.allowed_to?(:file_manipulation, @project) && + User.current.allowed_to?(:folder_manipulation, @project) + @email_allowed = User.current.allowed_to?(:email_documents, @project) + end + @back_url = params['back_url'].presence || dmsf_folder_path(id: @project, folder_id: @folder) + @notifications = Setting.notified_events.include?('dmsf_legacy_notifications') + render layout: false + rescue ActiveRecord::RecordNotFound + render_404 + end + + def trash + if @dmsf_file + @project = @dmsf_file.project + @allowed_restore = User.current.allowed_to? :file_manipulation, @project + @allowed_delete = User.current.allowed_to? :file_delete, @project + elsif @dmsf_folder + @project = @dmsf_folder.project + @allowed_restore = User.current.allowed_to? :folder_manipulation, @project + @allowed_delete = @allowed_restore + elsif @dmsf_link # url link + @project = @dmsf_link.project + @allowed_restore = User.current.allowed_to? :file_manipulation, @project + @allowed_delete = User.current.allowed_to? :file_delete, @project + else # multiple selection + @project = project + @allowed_restore = User.current.allowed_to?(:file_manipulation, @project) && + User.current.allowed_to?(:folder_manipulation, @project) + @allowed_delete = User.current.allowed_to?(:file_delete, @project) && + User.current.allowed_to?(:folder_manipulation, @project) + end + render layout: false + rescue ActiveRecord::RecordNotFound + render_404 + end + + private + + def project + prj = nil + params[:ids].each do |id| + item = case id + when /file-(\d+)/ + DmsfFile.find_by(id: Regexp.last_match(1)) + when /(file|url)-link-(\d+)/ + DmsfLink.find_by(id: Regexp.last_match(2)) + when /folder-(\d+)/ + DmsfFolder.find_by(id: Regexp.last_match(1)) + when /folder-link-(\d+)/ + DmsfLink.find_by(id: Regexp.last_match(1)) + end + prj ||= item&.project + return nil if prj != item&.project + end + prj + end + + def find_folder + @folder = DmsfFolder.find params[:folder_id] if params[:folder_id].present? + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_dmsf_file + return unless params[:ids].present? && params[:ids].size == 1 && !@dmsf_folder + + case params[:ids][0] + when /file-(\d+)/ + @dmsf_file = DmsfFile.find_by(id: Regexp.last_match(1)) + when /(file|url)-link-(\d+)/ + @dmsf_link = DmsfLink.find_by(id: Regexp.last_match(2)) + @dmsf_file = DmsfFile.find_by(id: @dmsf_link.target_id) if @dmsf_link && @dmsf_link.target_type != 'DmsfUrl' + end + end + + def find_dmsf_folder + return unless params[:ids].present? && params[:ids].size == 1 && !@dmsf_file + + case params[:ids][0] + when /folder-(\d+)/ + @dmsf_folder = DmsfFolder.find_by(id: Regexp.last_match(1)) + when /folder-link-(\d+)/ + @dmsf_link = DmsfLink.find_by(id: Regexp.last_match(1)) + @dmsf_folder = DmsfFolder.find_by(id: @dmsf_link.target_id) if @dmsf_link + end + end + + def find_dmsf_project + return unless params[:ids].present? && params[:ids].size == 1 && !@dmsf_project && + params[:ids][0] =~ /project-(\d+)/ + + @dmsf_project = Project.find_by(id: Regexp.last_match(1)) + end +end diff --git a/app/controllers/dmsf_controller.rb b/app/controllers/dmsf_controller.rb new file mode 100644 index 00000000..d519770d --- /dev/null +++ b/app/controllers/dmsf_controller.rb @@ -0,0 +1,855 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/dmsf_zip" + +# DMSF controller +class DmsfController < ApplicationController + include RedmineDmsf::DmsfZip + + before_action :find_project, except: %i[expand_folder index digest reset_digest] + before_action :authorize, except: %i[expand_folder index digest reset_digest] + before_action :authorize_global, only: [:index] + before_action :find_folder, + except: %i[new create edit_root save_root add_email append_email autocomplete_for_user digest + reset_digest] + before_action :find_parent, only: %i[new create delete] + before_action :permissions? + # Also try to lookup folder by title if this is an API call + before_action :find_folder_by_title, only: [:show] + before_action :query, only: %i[expand_folder show trash empty_trash index] + before_action :project_roles, only: %i[new edit create save] + before_action :find_target_folder, only: %i[copymove entries_operation] + before_action :check_target_folder, only: [:entries_operation] + before_action :require_login, only: %i[digest reset_digest] + + accept_api_auth :show, :create, :save, :delete, :entries_operation + + helper :custom_fields + helper :dmsf_folder_permissions + helper :queries + include QueriesHelper + helper :dmsf_queries + include DmsfQueriesHelper + helper :context_menus + helper :watchers + + def permissions? + if !DmsfFolder.permissions?(@folder, allow_system: false) + render_403 + elsif @folder && @project && (@folder.project != @project) + render_404 + end + true + end + + def expand_folder + @idnt = params[:idnt].present? ? params[:idnt].to_i + 1 : 0 + @query.project = Project.find_by(id: params[:project_id]) if params[:project_id].present? + @query.dmsf_folder_id = @folder&.id + @query.deleted = false + @query.sub_projects = true + respond_to do |format| + format.js { render action: 'query_rows' } + end + end + + def index + @query.sub_projects = true + show + end + + def show + @system_folder = @folder&.system + @locked = @folder&.locked? + @folder_manipulation_allowed = User.current.allowed_to?(:folder_manipulation, @project) + @file_manipulation_allowed = User.current.allowed_to?(:file_manipulation, @project) + @trash_enabled = @folder_manipulation_allowed && @file_manipulation_allowed + @notifications = Setting.notified_events.include?('dmsf_legacy_notifications') + @query.dmsf_folder_id = @folder&.id + @query.deleted = false + @query.sub_projects |= RedmineDmsf.dmsf_projects_as_subfolders? + if @folder&.deleted? || (params[:folder_title].present? && !@folder) + render_404 + return + end + if @query.valid? + respond_to do |format| + format.html do + # Warn about searching in sub-folders + if @folder && params['set_filter'].present? && params['f'].present? + flash.now[:warning] = l(:notice_search_in_subfolders) + end + @dmsf_count = @query.dmsf_count + @dmsf_pages = Paginator.new @dmsf_count, per_page_option, params['page'] + render layout: !request.xhr? + end + format.api { @offset, @limit = api_offset_and_limit } + format.csv do + send_data query_to_csv(@query.dmsf_nodes, @query), type: 'text/csv; header=present', filename: 'dmsf.csv' + end + end + else + respond_to do |format| + format.html do + @dmsf_count = 0 + @dmsf_pages = Paginator.new @dmsf_count, per_page_option, params['page'] + render layout: !request.xhr? + end + format.any(:atom, :csv, :pdf) { head :unprocessable_entity } + format.api { render_validation_errors(@query) } + end + end + end + + def trash + @folder_manipulation_allowed = User.current.allowed_to? :folder_manipulation, @project + @file_manipulation_allowed = User.current.allowed_to? :file_manipulation, @project + @file_delete_allowed = User.current.allowed_to? :file_delete, @project + @query.deleted = true + respond_to do |format| + format.html do + @dmsf_count = @query.dmsf_count + @dmsf_pages = Paginator.new @dmsf_count, per_page_option, params['page'] + render layout: !request.xhr? + end + end + end + + def download_email_entries + file_path = helpers.email_entry_tmp_file_path(params[:entry]) + if File.exist?(file_path) + # IE has got a tendency to cache files + expires_in 0.years, 'must-revalidate' => true + send_file( + file_path, + filename: 'Documents.zip', + type: 'application/zip', + disposition: 'attachment' + ) + else + render_404 + end + rescue StandardError => e + flash[:error] = e.message + end + + def copymove + @ids = params[:ids].uniq + member = Member.find_by(project_id: @project.id, user_id: User.current.id) + @fast_links = member&.dmsf_fast_links + unless @fast_links + @projects = DmsfFolder.allowed_target_projects_on_copy + @folders = DmsfFolder.directory_tree(@target_project) + @target_folder = DmsfFolder.visible.find(params[:target_folder_id]) if params[:target_folder_id].present? + end + @back_url = params[:back_url] + render layout: !request.xhr? + end + + def entries_operation + # Download/Email + if @selected_folders.blank? && @selected_files.blank? && @selected_links.blank? + flash[:warning] = l(:warning_no_entries_selected) + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + return + end + + if @selected_dir_links.present? && (params[:email_entries].present? || params[:download_entries].present?) + @selected_folders = DmsfLink.where(id: @selected_dir_links).pluck(:target_id) | @selected_folders + end + + if @selected_file_links.present? && (params[:email_entries].present? || params[:download_entries].present?) + @selected_files = DmsfLink.where(id: @selected_file_links).pluck(:target_id) | @selected_files + end + + begin + if params[:email_entries].present? + email_entries @selected_folders, @selected_files + elsif params[:restore_entries].present? + restore_entries @selected_folders, @selected_files, @selected_links + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + elsif params[:delete_entries].present? + delete_entries @selected_folders, @selected_files, @selected_links, false + redirect_back_or_default dmsf_folder_path id: @project, folder_id: @folder + elsif params[:destroy_entries].present? + delete_entries @selected_folders, @selected_files, @selected_links, true + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + elsif params[:copy_entries].present? + copy_entries @selected_folders, @selected_files, @selected_links + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + elsif params[:move_entries].present? + move_entries @selected_folders, @selected_files, @selected_links + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + else + download_entries @selected_folders, @selected_files + end + rescue DmsfFileNotFoundError + render_404 + rescue DmsfAccessError + render_403 + rescue StandardError => e + flash[:error] = e.message + Rails.logger.error e.message + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + end + + def entries_email + file_path = helpers.email_entry_tmp_file_path(params[:email][:zipped_content]) + params[:email][:zipped_content] = file_path + if params[:email][:to].strip.blank? + flash[:error] = l(:error_email_to_must_be_entered) + else + DmsfMailer.deliver_send_documents @project, params[:email].permit!, User.current + if File.exist?(file_path) + File.delete(file_path) + else + flash[:error] = l(:header_minimum_filesize) + end + flash[:notice] = l(:notice_email_sent, params[:email][:to]) + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def new + @folder = DmsfFolder.new + @pathfolder = @parent + render action: 'edit' + end + + def edit + @parent = @folder.dmsf_folder + @pathfolder = copy_folder(@folder) + @force_file_unlock_allowed = User.current.allowed_to?(:force_file_unlock, @project) + @redirect_to_folder_id = params[:redirect_to_folder_id] + @notifications = Setting.notified_events.include?('dmsf_legacy_notifications') + end + + def edit_root + @notifications = Setting.notified_events.include?('dmsf_legacy_notifications') + end + + def create + @folder = DmsfFolder.new + @folder.project = @project + @folder.user = User.current + saved = @folder.update_from_params(params) + respond_to do |format| + format.js + format.api { render_validation_errors(@folder) unless saved } + format.html do + if saved + flash[:notice] = l(:notice_folder_created) + redirect_to dmsf_folder_path(id: @project, folder_id: @folder.dmsf_folder) + else + @pathfolder = @parent + render action: 'edit' + end + end + end + end + + def save + unless params[:dmsf_folder] + redirect_to dmsf_folder_path(id: @project, folder_id: @folder) + return + end + @pathfolder = copy_folder(@folder) + saved = @folder.update_from_params(params) + respond_to do |format| + format.api { render_validation_errors(@folder) unless saved } + format.html do + if saved + flash[:notice] = l(:notice_folder_details_were_saved) + if @folder.project == @project + redirect_to_folder_id = params[:dmsf_folder][:redirect_to_folder_id] + redirect_to_folder_id = @folder.dmsf_folder.id if @folder.dmsf_folder && redirect_to_folder_id.blank? + end + redirect_to dmsf_folder_path(id: @project, folder_id: redirect_to_folder_id) + else + render action: 'edit' + end + end + end + end + + def delete + commit = params[:commit] == 'yes' + result = @folder.delete(commit: commit) + if result + flash[:notice] = l(:notice_folder_deleted) + else + flash[:error] = @folder.errors.full_messages.to_sentence + end + respond_to do |format| + format.html do + if commit + redirect_to trash_dmsf_path(@project) + else + redirect_to dmsf_folder_path(id: @project, folder_id: @parent) + end + end + format.api { result ? render_api_ok : render_validation_errors(@folder) } + end + end + + def restore + if @folder.restore + flash[:notice] = l(:notice_dmsf_folder_restored) + else + flash[:error] = @folder.errors.full_messages.to_sentence + end + redirect_to trash_dmsf_path(@project) + end + + def save_root + if params[:project] + @project.dmsf_description = params[:project][:dmsf_description] + if @project.save + flash[:notice] = l(:notice_folder_details_were_saved) + else + flash[:error] = @project.errors.full_messages.to_sentence + end + end + redirect_to dmsf_folder_path(id: @project) + end + + def notify_activate + if @folder&.notification || (@folder.nil? && @project.dmsf_notification) + flash[:warning] = l(:warning_folder_notifications_already_activated) + else + if @folder + @folder.notify_activate + else + @project.dmsf_notification = true + @project.save! + end + flash[:notice] = l(:notice_folder_notifications_activated) + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def notify_deactivate + if (@folder && !@folder.notification) || (@folder.nil? && !@project.dmsf_notification) + flash[:warning] = l(:warning_folder_notifications_already_deactivated) + else + if @folder + @folder.notify_deactivate + else + @project.dmsf_notification = nil + @project.save! + end + flash[:notice] = l(:notice_folder_notifications_deactivated) + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def lock + if @folder.nil? + flash[:warning] = l(:warning_folder_unlockable) + elsif @folder.locked? + flash[:warning] = l(:warning_folder_already_locked) + else + @folder.lock! + flash[:notice] = l(:notice_folder_locked) + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def unlock + if @folder.nil? + flash[:warning] = l(:warning_folder_unlockable) + elsif !@folder.locked? + flash[:warning] = l(:warning_folder_not_locked) + elsif @folder.locks[0].user == User.current || User.current.allowed_to?(:force_file_unlock, @project) + @folder.unlock! + flash[:notice] = l(:notice_folder_unlocked) + else + flash[:error] = l(:error_only_user_that_locked_folder_can_unlock_it) + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def add_email + @principals = users_for_new_users + end + + def append_email + @principals = Principal.where(id: params[:user_ids]).to_a + head :success if @principals.blank? + end + + def autocomplete_for_user + @principals = users_for_new_users + respond_to do |format| + format.js + end + end + + # Move the dragged object to the given destination + def drop + result = false + object = nil + if params[:dmsf_folder].present? && + params[:dmsf_folder][:drag_id].present? && + params[:dmsf_folder][:drop_id].present? && + params[:dmsf_folder][:drag_id] =~ /(.+)-(\d+)/ + type = Regexp.last_match(1) + id = Regexp.last_match(2) + if params[:dmsf_folder][:drop_id] =~ /^(\d+)(p|f)span$/ + case type + when 'file' + object = DmsfFile.find_by(id: id) + when 'folder' + object = DmsfFolder.find_by(id: id) + when 'file-link', 'folder-link', 'url-link' + object = DmsfLink.find_by(id: id) + end + if object + case Regexp.last_match(2) + when 'p' + project = Project.find_by(id: Regexp.last_match(1)) + if project && + User.current.allowed_to?(:file_manipulation, project) && + User.current.allowed_to?(:folder_manipulation, project) + result = object.move_to(project, nil) + end + when 'f' + dmsf_folder = DmsfFolder.find_by(id: Regexp.last_match(1)) + if dmsf_folder + if dmsf_folder == object.dmsf_folder + object.errors.add(:base, l(:error_target_folder_same)) + elsif object.dmsf_folder&.locked_for_user? + object.errors.add(:base, l(:error_folder_is_locked)) + else + result = object.move_to(dmsf_folder.project, dmsf_folder) + end + end + end + end + end + end + respond_to do |format| + if result + format.js { head :ok } + else + format.js do + if object + flash.now[:error] = object.errors.full_messages.to_sentence + else + head :unprocessable_entity + end + end + end + end + end + + def empty_trash + @query.deleted = true + @query.dmsf_nodes.each do |node| + case node.type + when 'folder' + folder = DmsfFolder.find_by(id: node.id) + folder&.delete commit: true + when 'file' + file = DmsfFile.find_by(id: node.id) + file&.delete commit: true + when /link$/ + link = DmsfLink.find_by(id: node.id) + link&.delete commit: true + end + end + redirect_back_or_default trash_dmsf_path id: @project + end + + # Reset WebDAV digest + def digest; end + + def reset_digest + if request.post? + raise StandardError, l(:notice_account_wrong_password) unless User.current.check_password?(params[:password]) + + # We have to create a token first to prevent an autogenerated token's value + token = Token.create!(user_id: User.current.id, action: 'dmsf_webdav_digest') + token.value = ActiveSupport::Digest.hexdigest( + "#{User.current.login}:#{RedmineDmsf::Webdav::AUTHENTICATION_REALM}:#{params[:password]}" + ) + token.save + flash[:notice] = l(:notice_webdav_digest_reset) + end + rescue StandardError => e + flash[:error] = e.message + ensure + redirect_to my_account_path + end + + private + + def users_for_new_users + User.active.visible.member_of(@project).like(params[:q]).order(:type, :lastname).to_a + end + + def email_entries(selected_folders, selected_files) + raise DmsfAccessError unless User.current.allowed_to?(:email_documents, @project) + + zip = Zip.new + zip_entries(zip, selected_folders, selected_files) + zipped_content = zip.finish + + max_filesize = RedmineDmsf.dmsf_max_email_filesize + raise DmsfEmailMaxFileSizeError if max_filesize.positive? && File.size(zipped_content) > max_filesize * 1_048_576 + + zip.dmsf_files.each do |f| + # Action + audit = DmsfFileRevisionAccess.new + audit.user = User.current + audit.dmsf_file_revision = f.last_revision + audit.action = DmsfFileRevisionAccess::EMAIL_ACTION + audit.save! + end + + # Notification + begin + DmsfMailer.deliver_files_downloaded @project, zip.dmsf_files, request.remote_ip + rescue StandardError => e + Rails.logger.error "Could not send email notifications: #{e.message}" + end + + @email_params = { + zipped_content: helpers.tmp_entry_identifier(zipped_content), + folders: selected_folders, + files: selected_files, + subject: "#{@project.name} #{l(:label_dmsf_file_plural).downcase}", + from: RedmineDmsf.dmsf_documents_email_from, + reply_to: RedmineDmsf.dmsf_documents_email_reply_to + } + @back_url = params[:back_url] + render action: 'email_entries' + ensure + zip&.close + end + + def download_entries(selected_folders, selected_files) + zip = Zip.new + zip_entries(zip, selected_folders, selected_files) + zip.dmsf_files.each do |f| + # Action + audit = DmsfFileRevisionAccess.new + audit.user = User.current + audit.dmsf_file_revision = f.last_revision + audit.action = DmsfFileRevisionAccess::DOWNLOAD_ACTION + audit.save! + end + # Notifications + begin + DmsfMailer.deliver_files_downloaded @project, zip.dmsf_files, request.remote_ip + rescue StandardError => e + Rails.logger.error "Could not send email notifications: #{e.message}" + end + send_file( + zip.finish, + filename: filename_for_content_disposition("#{@project.name}-#{DateTime.current.strftime('%y%m%d%H%M%S')}.zip"), + type: 'application/zip', + disposition: 'attachment' + ) + ensure + zip&.close + end + + def zip_entries(zip, selected_folders, selected_files) + member = Member.find_by(user_id: User.current.id, project_id: @project.id) + selected_folders.each do |selected_folder_id| + folder = DmsfFolder.visible.find_by(id: selected_folder_id) + raise DmsfFileNotFoundError unless folder + + zip.add_dmsf_folder folder, member, folder&.dmsf_folder&.dmsf_path_str + 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) + + unless (file.project == @project) || User.current.allowed_to?(:view_dmsf_files, file.project) + raise DmsfAccessError + end + + zip.add_dmsf_file file, member, file.dmsf_folder&.dmsf_path_str + end + max_files = RedmineDmsf.dmsf_max_file_download + raise DmsfZipMaxFilesError if max_files.positive? && zip.dmsf_files.length > max_files + + zip + end + + def restore_entries(selected_folders, selected_files, selected_links) + # Folders + selected_folders.each do |id| + folder = DmsfFolder.find_by(id: id) + + raise DmsfFileNotFoundError unless folder + end + # Files + selected_files.each do |id| + file = DmsfFile.find_by(id: id) + raise DmsfFileNotFoundError unless file + + flash[:error] = file.errors.full_messages.to_sentence unless file.restore + end + # Links + selected_links.each do |id| + link = DmsfLink.find_by(id: id) + raise DmsfFileNotFoundError unless link + + flash[:error] = link.errors.full_messages.to_sentence unless link.restore + end + end + + def delete_entries(selected_folders, selected_files, selected_links, commit) + # Folders + selected_folders.each do |id| + raise DmsfAccessError unless User.current.allowed_to?(:folder_manipulation, @project) + + folder = DmsfFolder.find_by(id: id) + if folder + raise StandardError, folder.errors.full_messages.to_sentence unless folder.delete(commit: commit) + elsif !commit + raise DmsfFileNotFoundError + end + end + # Files + deleted_files = [] + not_deleted_files = [] + if selected_files.any? + raise DmsfAccessError unless User.current.allowed_to?(:file_delete, @project) + + selected_files.each do |id| + file = DmsfFile.find_by(id: id) + if file + if file.delete(commit: commit) + deleted_files << file unless commit + else + not_deleted_files << file + end + elsif !commit + raise DmsfFileNotFoundError + end + end + end + # Activities + unless deleted_files.empty? + begin + recipients = DmsfMailer.deliver_files_deleted(@project, deleted_files) + if RedmineDmsf.dmsf_display_notified_recipients? && recipients.any? + max_receivers = RedmineDmsf.dmsf_max_notification_receivers_info + to = recipients.collect { |user, _| user.name }.first(max_receivers).join(', ') + if to.present? + to << (recipients.count > max_receivers ? ',...' : '.') + flash[:warning] = l(:warning_email_notifications, to: to) + end + end + rescue StandardError => e + Rails.logger.error "Could not send email notifications: #{e.message}" + end + end + unless not_deleted_files.empty? + flash[:warning] = l(:warning_some_entries_were_not_deleted, entries: not_deleted_files.map(&:title).join(', ')) + end + # Links + if selected_links.any? + raise DmsfAccessError unless User.current.allowed_to?(:folder_manipulation, @project) + + selected_links.each do |id| + link = DmsfLink.find_by(id: id) + link&.delete commit: commit + end + end + flash[:notice] = l(:notice_entries_deleted) if flash[:error].blank? && flash[:warning].blank? + end + + def copy_entries(selected_folders, selected_files, selected_links) + # Folders + selected_folders.each do |id| + folder = DmsfFolder.find_by(id: id) + new_folder = folder.copy_to(@target_project, @target_folder) + raise(StandardError, new_folder.errors.full_messages.to_sentence) unless new_folder.errors.empty? + end + # Files + selected_files.each do |id| + file = DmsfFile.find_by(id: id) + new_file = file.copy_to(@target_project, @target_folder) + raise(StandardError, new_file.errors.full_messages.to_sentence) unless new_file.errors.empty? + end + # Links + selected_links.each do |id| + link = DmsfLink.find_by(id: id) + new_link = link.copy_to(@target_project, @target_folder) + raise(StandardError, new_link.errors.full_messages.to_sentence) unless new_link.errors.empty? + end + flash[:notice] = l(:notice_entries_copied) if flash[:error].blank? && flash[:warning].blank? + end + + def move_entries(selected_folders, selected_files, selected_links) + # Permissions + raise DmsfAccessError if selected_folders.any? && !User.current.allowed_to?(:folder_manipulation, @project) + + if (selected_folders.any? || selected_links.any?) && !User.current.allowed_to?(:file_manipulation, @project) + raise DmsfAccessError + end + + # Folders + selected_folders.each do |id| + folder = DmsfFolder.find_by(id: id) + unless folder.move_to(@target_project, @target_folder) + raise(StandardError, folder.errors.full_messages.to_sentence) + end + end + # Files + selected_files.each do |id| + file = DmsfFile.find_by(id: id) + raise(StandardError, file.errors.full_messages.to_sentence) unless file.move_to(@target_project, @target_folder) + end + # Links + selected_links.each do |id| + link = DmsfLink.find_by(id: id) + raise(StandardError, link.errors.full_messages.to_sentence) unless link.move_to(@target_project, @target_folder) + end + flash[:notice] = l(:notice_entries_moved) if flash[:error].blank? && flash[:warning].blank? + end + + def find_folder + @folder = DmsfFolder.find(params[:folder_id]) if params[:folder_id].present? + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_folder_by_title + return unless api_request? && !@folder && params[:folder_title].present? + + @folder = DmsfFolder.find_by(title: params[:folder_title], project_id: @project.id) + render_404 unless @folder + end + + def find_parent + @parent = DmsfFolder.visible.find params[:parent_id] if params[:parent_id].present? + rescue ActiveRecord::RecordNotFound + render_404 + end + + def copy_folder(folder) + copy = folder.clone + copy.id = folder.id + copy + end + + def query + retrieve_default_query true + @query = retrieve_query DmsfQuery, true + end + + def retrieve_default_query(use_session) + return if params[:query_id].present? + return if api_request? + return if params[:set_filter] + + if params[:without_default].present? + params[:set_filter] = 1 + return + end + if !params[:set_filter] && use_session && session[:issue_query] + query_id, project_id = session[:issue_query].values_at(:id, :project_id) + return if DmsfQuery.exists?(id: query_id) && project_id == @project&.id + end + default_query = DmsfQuery.default(project: @project) + return unless default_query + + params[:query_id] = default_query.id + end + + def project_roles + @project_roles = Role.givable.joins(:member_roles).joins(:members).where(members: { project_id: @project.id }) + .distinct + end + + def find_target_folder + @target_project = if params[:dmsf_entries] && params[:dmsf_entries][:target_project_id].present? + Project.find params[:dmsf_entries][:target_project_id] + else + @project + end + if params[:dmsf_entries] && params[:dmsf_entries][:target_folder_id].present? + target_folder_id = params[:dmsf_entries][:target_folder_id] + @target_folder = DmsfFolder.find(target_folder_id) + raise ActiveRecord::RecordNotFound unless DmsfFolder.visible.exists?(id: target_folder_id) + + @target_project = @target_folder&.project + end + rescue ActiveRecord::RecordNotFound + render_404 + end + + def check_target_folder + if params[:ids].present? + @selected_folders = params[:ids].grep(/folder-\d+/).map { |x| Regexp.last_match(1).to_i if x =~ /folder-(\d+)/ } + @selected_files = params[:ids].grep(/file-\d+/).map { |x| Regexp.last_match(1).to_i if x =~ /file-(\d+)/ } + @selected_dir_links = params[:ids].grep(/folder-link-\d+/) + .map { |x| Regexp.last_match(1).to_i if x =~ /folder-link-(\d+)/ } + @selected_file_links = params[:ids].grep(/file-link-\d+/) + .map { |x| Regexp.last_match(1).to_i if x =~ /file-link-(\d+)/ } + @selected_url_links = params[:ids].grep(/url-link-\d+/) + .map { |x| Regexp.last_match(1).to_i if x =~ /url-link-(\d+)/ } + @selected_links = @selected_dir_links + @selected_file_links + @selected_url_links + else + @selected_folders = [] + @selected_files = [] + @selected_links = [] + end + return unless params[:copy_entries].present? || params[:move_entries].present? + + begin + # Prevent copying/moving to the same destination + folders = DmsfFolder.where(id: @selected_folders).to_a + files = DmsfFile.where(id: @selected_files).to_a + links = DmsfLink.where(id: @selected_links).to_a + (folders + files + links).each do |entry| + if entry.dmsf_folder + raise DmsfParentError if entry.dmsf_folder == @target_folder || entry == @target_folder + elsif @target_folder.nil? + raise DmsfParentError if entry.project == @target_project + end + end + # Prevent recursion + if params[:move_entries].present? + folders.each do |entry| + raise DmsfParentError if entry.any_child?(@target_folder) + end + end + # Check permissions + if (@target_folder && (@target_folder.locked_for_user? || + !DmsfFolder.permissions?(@target_folder, allow_system: false))) || + !@target_project.allows_to?(:folder_manipulation) + raise DmsfAccessError + end + rescue DmsfParentError + flash[:error] = l(:error_target_folder_same) + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + rescue DmsfAccessError + render_403 + end + end +end diff --git a/app/controllers/dmsf_files_controller.rb b/app/controllers/dmsf_files_controller.rb new file mode 100644 index 00000000..49fe0451 --- /dev/null +++ b/app/controllers/dmsf_files_controller.rb @@ -0,0 +1,378 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Files controller +class DmsfFilesController < ApplicationController + menu_item :dmsf + + before_action :find_file, except: %i[delete_revision obsolete_revision] + before_action :find_revision, only: %i[delete_revision obsolete_revision] + before_action :find_folder, only: %i[delete create_revision] + before_action :authorize + before_action :permissions? + + accept_api_auth :show, :view, :delete, :create_revision + + helper :custom_fields + helper :dmsf_workflows + helper :dmsf + helper :queries + helper :watchers + helper :context_menus + + include QueriesHelper + + def permissions? + render_403 if @file && !DmsfFolder.permissions?(@file.dmsf_folder, allow_system: true, file: true) + true + end + + def view + if params[:download].blank? + @revision = @file.last_revision + else + @revision = DmsfFileRevision.find(params[:download].to_i) + raise DmsfAccessError if @revision.dmsf_file != @file + end + + check_project @revision.dmsf_file + raise ActionController::MissingFile if @file.deleted? + + # Action + access = DmsfFileRevisionAccess.new + access.user = User.current + access.dmsf_file_revision = @revision + access.action = DmsfFileRevisionAccess::DOWNLOAD_ACTION + access.save! + # Notifications + begin + DmsfMailer.deliver_files_downloaded @project, [@file], request.remote_ip + rescue StandardError => e + 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 }) + return if results.first == true + + member = Member.find_by(user_id: User.current.id, project_id: @file.project.id) + # IE has got a tendency to cache files + expires_in 0.years, 'must-revalidate' => true + # PDF preview + pdf_preview = (params[:disposition] != 'attachment') && params[:filename].blank? && @file.pdf_preview + filename = filename_for_content_disposition(@revision.formatted_name(member)) + if !api_request? && pdf_preview.present? && (RedmineDmsf.office_bin.present? || params[:preview].present?) + basename = File.basename(filename, '.*') + send_file pdf_preview, filename: "#{basename}.pdf", type: 'application/pdf', disposition: 'inline' + # 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') + render action: 'document' + # Offer the file for download + else + params[:disposition] = 'attachment' if params[:filename].present? + send_file @revision.disk_file, + filename: filename, + type: @revision.detect_content_type, + disposition: params[:disposition].presence || @revision.dmsf_file.disposition + end + rescue DmsfAccessError => e + Rails.logger.error e.message + render_403 + rescue StandardError => e + Rails.logger.error e.message + render_404 + end + + def show + @revision = @file.last_revision + @file_delete_allowed = User.current.allowed_to?(:file_delete, @project) + @file_manipulation_allowed = User.current.allowed_to?(:file_manipulation, @project) + @revision_count = @file.dmsf_file_revisions.visible.all.size + @revision_pages = Paginator.new @revision_count, params['per_page'] ? params['per_page'].to_i : 25, params['page'] + @notifications = Setting.notified_events.include?('dmsf_legacy_notifications') + + respond_to do |format| + format.html { render layout: !request.xhr? } + format.api + end + end + + def create_revision + if params[:dmsf_file_revision] && !@file.locked_for_user? + revision = DmsfFileRevision.new + revision.title = params[:dmsf_file_revision][:title].scrub.strip + revision.name = params[:dmsf_file_revision][:name].scrub.strip + revision.description = params[:dmsf_file_revision][:description]&.scrub&.strip + revision.comment = params[:dmsf_file_revision][:comment]&.scrub&.strip + revision.dmsf_file = @file + last_revision = @file.last_revision + revision.source_revision = last_revision + revision.user = User.current + + # Version + revision.major_version = DmsfUploadHelper.db_version(params[:version_major]) + revision.minor_version = DmsfUploadHelper.db_version(params[:version_minor]) + revision.patch_version = DmsfUploadHelper.db_version(params[:version_patch]) + + 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 + 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? + begin + @file.unlock! + flash[:notice] = "#{l(:notice_file_unlocked)}, " + rescue StandardError => e + Rails.logger.error "Cannot unlock the file: #{e.message}" + ok = false + end + end + if ok && @file.save + @file.last_revision = revision + call_hook :dmsf_helper_upload_after_commit, { file: @file } + begin + recipients = DmsfMailer.deliver_files_updated(@project, [@file]) + if RedmineDmsf.dmsf_display_notified_recipients? && recipients.any? + max_notifications = RedmineDmsf.dmsf_max_notification_receivers_info + to = recipients.collect { |user, _| user.name }.first(max_notifications).join(', ') + if to.present? + to << (recipients.count > max_notifications ? ',...' : '.') + end + end + rescue StandardError => e + Rails.logger.error "Could not send email notifications: #{e.message}" + end + else + ok = false + end + else + ok = false + end + end + + respond_to do |format| + format.html do + flash[:error] = l(:error_file_is_locked) if @file.locked_for_user? + flash[:warning] = l(:warning_email_notifications, to: to) if to.present? + flash[:error] = @file.errors.full_messages.to_sentence if @file.errors.any? + flash[:error] = revision.errors.full_messages.to_sentence if revision.errors.any? + flash[:notice] = (flash[:notice].nil? ? '' : flash[:notice]) + l(:notice_file_revision_created) if ok + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + format.api + end + end + + def delete + if @file + commit = params[:commit] == 'yes' + result = @file.delete(commit: commit) + if result + flash[:notice] = l(:notice_file_deleted) + if commit + container = @file.container + container&.dmsf_file_removed @file + else + begin + recipients = DmsfMailer.deliver_files_deleted(@project, [@file]) + if RedmineDmsf.dmsf_display_notified_recipients? && recipients.any? + max_notification = RedmineDmsf.dmsf_max_notification_receivers_info + to = recipients.collect { |user, _| user.name }.first(max_notification).join(', ') + if to.present? + to << (recipients.count > max_notification ? ',...' : '.') + flash[:warning] = l(:warning_email_notifications, to: to) + end + end + rescue StandardError => e + Rails.logger.error "Could not send email notifications: #{e.message}" + end + end + else + msg = @file.errors.full_messages.to_sentence + flash[:error] = msg + Rails.logger.error msg + end + end + respond_to do |format| + format.html { redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) } + format.api { result ? render_api_ok : render_validation_errors(@file) } + end + end + + def delete_revision + if @revision + if @revision.delete commit: true + if @file.name != @file.last_revision.name + @file.name = @file.last_revision.name + @file.save! + end + flash[:notice] = l(:notice_revision_deleted) + else + flash[:error] = @revision.errors.full_messages.to_sentence + end + end + redirect_to action: 'show', id: @file + end + + def obsolete_revision + if @revision + if @revision.obsolete + flash[:notice] = l(:notice_revision_obsoleted) + else + flash[:error] = @revision.errors.full_messages.to_sentence + end + end + redirect_to action: 'show', id: @file + end + + def lock + if @file.locked? + flash[:warning] = l(:warning_file_already_locked) + else + begin + @file.lock! + flash[:notice] = l(:notice_file_locked) + rescue StandardError => e + flash[:error] = e.message + end + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def unlock + if @file.locked? + if @file.locks[0].user == User.current || User.current.allowed_to?(:force_file_unlock, @file.project) + begin + @file.unlock! + flash[:notice] = l(:notice_file_unlocked) + rescue StandardError => e + flash[:error] = e.message + end + else + flash[:error] = l(:error_only_user_that_locked_file_can_unlock_it) + end + else + flash[:warning] = l(:warning_file_not_locked) + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def notify_activate + if @file.notification + flash[:warning] = l(:warning_file_notifications_already_activated) + else + @file.notify_activate + flash[:notice] = l(:notice_file_notifications_activated) + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def notify_deactivate + if @file.notification + @file.notify_deactivate + flash[:notice] = l(:notice_file_notifications_deactivated) + else + flash[:warning] = l(:warning_file_notifications_already_deactivated) + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def restore + if @file.restore + flash[:notice] = l(:notice_dmsf_file_restored) + else + flash[:error] = @file.errors.full_messages.to_sentence + end + 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 + @file = DmsfFile.find params[:id] + @project = @file.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_revision + @revision = DmsfFileRevision.visible.find params[:id] + @file = @revision.dmsf_file + @project = @file.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_folder + @folder = DmsfFolder.find params[:folder_id] if params[:folder_id].present? + rescue ActiveRecord::RecordNotFound + render_404 + end + + def check_project(entry) + return unless entry && entry.project != @project + + raise DmsfAccessError, l(:error_entry_project_does_not_match_current_project) + end +end diff --git a/app/controllers/dmsf_folder_permissions_controller.rb b/app/controllers/dmsf_folder_permissions_controller.rb new file mode 100644 index 00000000..fe50c537 --- /dev/null +++ b/app/controllers/dmsf_folder_permissions_controller.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Folder permissions controller +class DmsfFolderPermissionsController < ApplicationController + before_action :find_folder, + only: %i[destroy new autocomplete_for_user], + if: -> { params[:dmsf_folder_id].present? } + before_action :find_project + before_action :authorize + before_action :permissions? + + helper :dmsf + + def permissions? + render_403 unless DmsfFolder.permissions?(@dmsf_folder) + true + end + + def new + @principals = users_for_new_users + end + + def append + @principals = Principal.where(id: params[:user_ids]).to_a + head :ok if @principals.blank? + end + + def autocomplete_for_user + @principals = users_for_new_users + respond_to do |format| + format.js + end + end + + private + + def users_for_new_users + scope = Principal.active.visible.member_of(@project).like(params[:q]).order(:type, :lastname) + if @dmsf_folder + users = @dmsf_folder.permissions_users + if users.any? + ids = users.collect(&:id) + scope = scope.where.not(id: ids) + end + end + scope.to_a + end + + def find_project + @project = Project.visible.find_by_param(params[:project_id]) + rescue DmsfAccessError + render_403 + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_folder + @dmsf_folder = DmsfFolder.visible.find(params[:dmsf_folder_id]) + rescue DmsfAccessError + render_403 + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/app/controllers/dmsf_help_controller.rb b/app/controllers/dmsf_help_controller.rb new file mode 100644 index 00000000..14e415b9 --- /dev/null +++ b/app/controllers/dmsf_help_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Help controller +class DmsfHelpController < ApplicationController + def show_wiki_syntax + lang = current_language.to_s + template = "dmsf_help/#{lang}/wiki_syntax" + lang = 'en' unless lookup_context.exists?(template) + render template: "dmsf_help/#{lang}/wiki_syntax", layout: nil + end + + def show_dmsf_help + lang = current_language.to_s + template = "dmsf_help/#{lang}/dmsf_help" + lang = 'en' unless lookup_context.exists?(template) + render template: "dmsf_help/#{lang}/dmsf_help", layout: nil + end +end diff --git a/app/controllers/dmsf_links_controller.rb b/app/controllers/dmsf_links_controller.rb new file mode 100644 index 00000000..54f849a5 --- /dev/null +++ b/app/controllers/dmsf_links_controller.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Links controller +class DmsfLinksController < ApplicationController + model_object DmsfLink + + before_action :find_model_object, only: %i[destroy restore] + before_action :find_link_project + before_action :find_folder, only: [:destroy] + before_action :authorize + before_action :permissions? + + protect_from_forgery except: :new + + accept_api_auth :create + + helper :dmsf + + def permissions? + render_403 if @dmsf_link && !DmsfFolder.permissions?(@dmsf_link.dmsf_folder) + true + end + + def initialize + @dmsf_link = nil + super + end + + def new + @dmsf_link = DmsfLink.new + member = Member.find_by(project_id: params[:project_id], user_id: User.current.id) + @fast_links = member&.dmsf_fast_links + @dmsf_link.project_id = params[:project_id] + @dmsf_link.dmsf_folder_id = params[:dmsf_folder_id] + @dmsf_file_id = params[:dmsf_file_id] + @type = params[:type] + @dmsf_link.target_project_id = params[:project_id] + @back_url = params[:back_url] + if @type == 'link_to' + if @dmsf_file_id + f = DmsfFile.find_by(id: @dmsf_file_id) + @dmsf_link.name = f&.last_revision&.title + else + titles = DmsfFolder.where(id: @dmsf_link.dmsf_folder_id).pluck(:title) + @dmsf_link.name = titles.first if titles.any? + end + end + @container = params[:container] + respond_to do |format| + format.html + format.js + end + end + + def autocomplete_for_project + respond_to do |format| + format.js + end + end + + def autocomplete_for_folder + respond_to do |format| + format.js + end + end + + def create + @dmsf_link = DmsfLink.new + @dmsf_link.user = User.current + if params[:dmsf_link][:type] == 'link_from' + # Link from + if params[:dmsf_link][:container].blank? + @dmsf_link.project_id = params[:dmsf_link][:project_id] + @dmsf_link.dmsf_folder_id = params[:dmsf_link][:dmsf_folder_id] + else + # A container link + @dmsf_link.project_id = -1 + @dmsf_link.dmsf_folder_id = nil + end + @dmsf_link.target_project_id = params[:dmsf_link][:target_project_id] + if params[:external_link] == 'true' + @dmsf_link.external_url = params[:dmsf_link][:external_url] + @dmsf_link.target_type = 'DmsfUrl' + elsif params[:dmsf_link][:target_file_id].present? + @dmsf_link.target_id = params[:dmsf_link][:target_file_id] + @dmsf_link.target_type = DmsfFile.model_name.to_s + else + @dmsf_link.target_id = if DmsfLinksHelper.number?(params[:dmsf_link][:target_folder_id]) + params[:dmsf_link][:target_folder_id].to_i + end + @dmsf_link.target_type = DmsfFolder.model_name.to_s + end + @dmsf_link.name = params[:dmsf_link][:name].scrub.strip + result = @dmsf_link.save + if result + flash[:notice] = l(:notice_successful_create) + else + msg = @dmsf_link.errors.full_messages.to_sentence + flash[:error] = msg + Rails.logger.error msg + end + else + # Link to + if DmsfLinksHelper.number?(params[:dmsf_link][:target_folder_id]) + @dmsf_link.dmsf_folder_id = params[:dmsf_link][:target_folder_id].to_i + end + if params[:dmsf_link][:target_project_id].present? + @dmsf_link.project_id = params[:dmsf_link][:target_project_id] + else + project_ids = DmsfFolder.where(id: params[:dmsf_link][:target_folder_id]).pluck(:project_id) + unless project_ids.any? + render_404 + return + end + @dmsf_link.project_id = project_ids.first + end + @dmsf_link.target_project_id = params[:dmsf_link][:project_id] + if params[:dmsf_link][:dmsf_file_id].present? + @dmsf_link.target_id = params[:dmsf_link][:dmsf_file_id] + @dmsf_link.target_type = DmsfFile.model_name.to_s + else + @dmsf_link.target_id = params[:dmsf_link][:dmsf_folder_id] + @dmsf_link.target_type = DmsfFolder.model_name.to_s + end + @dmsf_link.name = params[:dmsf_link][:name] + result = @dmsf_link.save + if result + flash[:notice] = l(:notice_successful_create) + else + flash[:error] = @dmsf_link.errors.full_messages.to_sentence + end + end + respond_to do |format| + format.html do + if params[:dmsf_link][:type] == 'link_from' + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @dmsf_link.dmsf_folder_id) + elsif params[:dmsf_link][:dmsf_file_id].present? + redirect_back_or_default dmsf_file_path(@dmsf_link.target_file) + else + folder = @dmsf_link.target_folder.dmsf_folder if @dmsf_link.target_folder + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: folder) + end + end + format.api { render_validation_errors(@dmsf_link) unless result } + format.js + end + end + + def destroy + begin + if @dmsf_link + commit = params[:commit] == 'yes' + if @dmsf_link.delete(commit: commit) + flash[:notice] = l(:notice_successful_delete) + else + flash[:error] = @dmsf_link.errors.full_messages.to_sentence + end + end + rescue StandardError => e + errors.add :base, e.message + return false + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def restore + if @dmsf_link.restore + flash[:notice] = l(:notice_dmsf_link_restored) + else + flash[:error] = @dmsf_link.errors.full_messages.to_sentence + end + redirect_to trash_dmsf_path(@project) + end + + private + + def l_params + params.fetch(:dmsf_link, {}).permit(:project_id) + end + + def find_link_project + if @dmsf_link + @project = @dmsf_link.project + else + pid = if params[:dmsf_link].present? + if params[:dmsf_link].fetch(:project_id, nil).blank? + params[:dmsf_link].fetch(:target_project_id, nil) + else + params[:dmsf_link][:project_id] + end + else + params[:project_id] + end + @project = Project.find(pid) + end + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_folder + @folder = DmsfFolder.find params[:folder_id] if params[:folder_id].present? + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/app/controllers/dmsf_public_urls_controller.rb b/app/controllers/dmsf_public_urls_controller.rb new file mode 100644 index 00000000..38b1faaf --- /dev/null +++ b/app/controllers/dmsf_public_urls_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Public URL controller +class DmsfPublicUrlsController < ApplicationController + model_object DmsfPublicUrl + before_action :authorize, only: [:create] + skip_before_action :check_if_login_required, only: [:show] + + def show + dmsf_public_url = DmsfPublicUrl.where('token = ? AND expire_at >= ?', params[:token], DateTime.current).first + if dmsf_public_url + 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, + filename: filename_for_content_disposition(revision.name), + type: revision.detect_content_type, + disposition: dmsf_public_url.dmsf_file.disposition) + rescue StandardError => e + Rails.logger.error e.message + render_404 + end + else + render_404 + end + end + + def create; end +end diff --git a/app/controllers/dmsf_state_controller.rb b/app/controllers/dmsf_state_controller.rb new file mode 100644 index 00000000..a168773c --- /dev/null +++ b/app/controllers/dmsf_state_controller.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# State controller +class DmsfStateController < ApplicationController + menu_item :dmsf + + before_action :find_project + before_action :authorize + + def user_pref_save + member = @project.members.find_by(user_id: User.current.id) + if member + if Setting.notified_events.include?('dmsf_legacy_notifications') + member.dmsf_mail_notification = params[:email_notify] + end + member.dmsf_title_format = params[:title_format] + member.dmsf_fast_links = params[:fast_links].present? + if format_valid?(member.dmsf_title_format) && member.save + flash[:notice] = l(:notice_your_preferences_were_saved) + else + flash[:error] = l(:notice_your_preferences_were_not_saved) + end + else + flash[:warning] = l(:user_is_not_project_member) + end + @project.update(dmsf_act_as_attachable: params[:act_as_attachable]) if RedmineDmsf.dmsf_act_as_attachable? + @project.update default_dmsf_query_id: params[:default_dmsf_query] + redirect_to settings_project_path(@project, tab: 'dmsf') + end + + private + + def format_valid?(format) + format.blank? || (/%(t|d|v|i|r)/.match?(format) && format.length < 256) + end +end diff --git a/app/controllers/dmsf_upload_controller.rb b/app/controllers/dmsf_upload_controller.rb new file mode 100644 index 00000000..97159471 --- /dev/null +++ b/app/controllers/dmsf_upload_controller.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Upload controller +class DmsfUploadController < ApplicationController + menu_item :dmsf + + before_action :find_project, except: %i[upload delete_dmsf_attachment delete_dmsf_link_attachment] + before_action :authorize, except: %i[upload delete_dmsf_attachment delete_dmsf_link_attachment] + before_action :authorize_global, only: %i[upload delete_dmsf_attachment delete_dmsf_link_attachment] + before_action :find_folder, except: %i[upload commit delete_dmsf_attachment delete_dmsf_link_attachment] + before_action :permissions?, except: %i[upload commit delete_dmsf_attachment delete_dmsf_link_attachment] + + helper :custom_fields + helper :dmsf_workflows + helper :dmsf + + accept_api_auth :upload, :commit + + def permissions? + render_403 unless DmsfFolder.permissions?(@folder) + true + end + + def upload_files + uploaded_files = params[:dmsf_attachments] + @uploads = [] + # Commit + if params[:commit] == l(:label_dmsf_upload_commit) + uploaded_files&.each do |key, uploaded_file| + upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, uploaded_file) + next unless upload + + @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? + # Upload + else + # standard file input uploads + uploaded_files&.each_value do |uploaded_file| + upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, uploaded_file) + @uploads.push(upload) if upload + end + end + flash.now[:error] = "#{l(:label_attachment)} #{l('activerecord.errors.messages.invalid')}" if @uploads.empty? + end + + # 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) + @attachment.content_type = params[:content_type].presence + begin + Attachment.skip_callback(:commit, :after, :reuse_existing_file_if_possible, raise: false) + saved = @attachment.save + ensure + Attachment.set_callback(:commit, :after, :reuse_existing_file_if_possible) + end + + respond_to do |format| + format.js + format.api do + if saved + render action: 'upload', status: :created + else + render_validation_errors @attachment + end + end + end + end + + def commit_files + commit_files_internal params[:committed_files] + end + + # REST API file commit + def commit + @files = [] + attachments = params[:attachments] + return unless attachments + + @folder = DmsfFolder.visible.find_by(id: attachments[:folder_id]) if attachments[:folder_id].present? + # standard file input uploads + uploaded_files = attachments.slice('uploaded_file') + uploaded_files.each_value do |uploaded_file| + upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, uploaded_file) + next unless upload + + 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 + end + + def delete_dmsf_attachment + attachment = Attachment.find(params[:id]) + attachment.destroy + rescue ActiveRecord::RecordNotFound + render_404 + end + + def delete_dmsf_link_attachment + link = DmsfLink.find(params[:id]) + link.destroy + rescue ActiveRecord::RecordNotFound + render_404 + end + + private + + def commit_files_internal(committed_files) + @files, failed_uploads = DmsfUploadHelper.commit_files_internal(committed_files, @project, @folder, self) + call_hook :dmsf_upload_controller_after_commit, { files: @files } + respond_to do |format| + format.js + format.api { render_validation_errors(failed_uploads) unless failed_uploads.empty? } + format.html { redirect_to dmsf_folder_path(id: @project, folder_id: @folder) } + end + end + + def find_folder + @folder = DmsfFolder.visible.find(params[:folder_id]) if params.key?('folder_id') + rescue DmsfAccessError + render_403 + end +end diff --git a/app/controllers/dmsf_workflows_controller.rb b/app/controllers/dmsf_workflows_controller.rb new file mode 100644 index 00000000..59003aad --- /dev/null +++ b/app/controllers/dmsf_workflows_controller.rb @@ -0,0 +1,544 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# WorkflowsController +class DmsfWorkflowsController < ApplicationController + model_object DmsfWorkflow + menu_item :dmsf_approvalworkflows + self.main_menu = false + + before_action :find_model_object, except: %i[create new index assign assignment] + before_action :find_project + before_action :authorize_custom + before_action :permissions?, only: %i[new_action assignment start] + before_action :approver_candidates, only: %i[remove_step show reorder_steps add_step] + before_action :prevent_from_editing, only: %i[destroy remove_step update add_step update_step reorder_steps] + + layout :workflows_layout + + helper :dmsf + + def permissions? + revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id]) if params[:dmsf_file_revision_id].present? + render_403 unless revision&.dmsf_file || DmsfFolder.permissions?(revision&.dmsf_file&.dmsf_folder) + true + end + + def initialize + @dmsf_workflow = nil + @project = nil + super + end + + def index + @status = params[:status] || 1 + @workflow_pages, @workflows = paginate(DmsfWorkflow.status(@status).global.sorted, per_page: 25) + @path = dmsf_workflows_path + end + + def action + # Noting to do + end + + def show + # Noting to do + end + + def new_action + unless params[:commit] == l(:button_submit) && request.post? + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + return + end + + action = DmsfWorkflowStepAction.new( + dmsf_workflow_step_assignment_id: params[:dmsf_workflow_step_assignment_id], + action: params[:step_action].to_i >= 10 ? DmsfWorkflowStepAction::ACTION_DELEGATE : params[:step_action], + note: params[:note] + ) + revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id]) + result = call_hook(:dmsf_workflow_controller_before_approval, + { dmsf_file_revision: revision, step_action: params[:step_action] }) + if (result.blank? || result.first) && action.save + if revision + if @dmsf_workflow.try_finish? revision, action, (params[:step_action].to_i / 10) + if revision.dmsf_file + begin + revision.dmsf_file.unlock!(force_file_unlock_allowed: true) unless RedmineDmsf.dmsf_keep_documents_locked? + rescue DmsfLockError => e + flash[:info] = e.message + end + end + if revision.workflow == DmsfWorkflow::STATE_APPROVED + # Just approved + if Setting.notified_events.include?('dmsf_workflow_plural') + recipients = DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true) + DmsfMailer.deliver_workflow_notification( + recipients, + @dmsf_workflow, + revision, + :text_email_subject_approved, + :text_email_finished_approved, + :text_email_to_see_history + ) + if RedmineDmsf.dmsf_display_notified_recipients? && recipients.present? + max_notifications = RedmineDmsf.dmsf_max_notification_receivers_info + to = recipients.collect(&:name).first(max_notifications).join(', ') + if to.present? + to << (recipients.count > max_notifications ? ',...' : '.') + flash[:warning] = l(:warning_email_notifications, to: to) + end + end + end + elsif Setting.notified_events.include?('dmsf_workflow_plural') # Just rejected + recipients = @dmsf_workflow.participiants + recipients.push revision.dmsf_workflow_assigned_by_user + recipients.uniq! + recipients &= DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true) + DmsfMailer.deliver_workflow_notification( + recipients, + @dmsf_workflow, + revision, + :text_email_subject_rejected, + :text_email_finished_rejected, + :text_email_to_see_history, + action.note + ) + if RedmineDmsf.dmsf_display_notified_recipients? && recipients.present? + max_notifications = RedmineDmsf.dmsf_max_notification_receivers_info + to = recipients.collect(&:name).first(max_notifications).join(', ') + if to.present? + to << (recipients.count > max_notifications ? ',...' : '.') + flash[:warning] = l(:warning_email_notifications, to: to) + end + end + end + elsif action.action == DmsfWorkflowStepAction::ACTION_DELEGATE + # Delegation + if Setting.notified_events.include?('dmsf_workflow_plural') + delegate = User.active.find_by(id: params[:step_action].to_i / 10) + if DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true).include?(delegate) + DmsfMailer.deliver_workflow_notification( + [delegate], + @dmsf_workflow, + revision, + :text_email_subject_delegated, + :text_email_finished_delegated, + :text_email_to_proceed, + action.note, + action.dmsf_workflow_step_assignment.dmsf_workflow_step + ) + if RedmineDmsf.dmsf_display_notified_recipients? + flash[:warning] = l(:warning_email_notifications, to: delegate.name) + end + end + end + else + # Next step + assignments = @dmsf_workflow.next_assignments revision.id + if assignments.any? && + Setting.notified_events.include?('dmsf_workflow_plural') && + assignments.first.dmsf_workflow_step.step != action.dmsf_workflow_step_assignment.dmsf_workflow_step.step + # Next step + assignments.each do |assignment| + next if assignment.user.nil? || + DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true) + .exclude?(assignment.user) + + DmsfMailer.deliver_workflow_notification( + [assignment.user], + @dmsf_workflow, + revision, + :text_email_subject_requires_approval, + :text_email_finished_step, + :text_email_to_proceed, + nil, + assignment.dmsf_workflow_step + ) + end + to = revision.dmsf_workflow_assigned_by_user + if to && DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true) + .include?(to) + DmsfMailer.deliver_workflow_notification( + [to], + @dmsf_workflow, + revision, + :text_email_subject_updated, + :text_email_finished_step_short, + :text_email_to_see_status + ) + end + if RedmineDmsf.dmsf_display_notified_recipients? + recipients = assignments.collect(&:user) + recipients << to if to + recipients.uniq! + recipients &= DmsfMailer.get_notify_users(@project, revision.dmsf_file, force_notification: true) + unless recipients.empty? + max_notifications = RedmineDmsf.dmsf_max_notification_receivers_info + to = recipients.collect(&:name).first(max_notifications).join(', ') + if to.present? + to << (recipients.count > max_notifications ? ',...' : '.') + flash[:warning] = l(:warning_email_notifications, to: to) + end + end + end + end + end + end + flash[:notice] = l(:notice_successful_update) + elsif action.action != DmsfWorkflowStepAction::ACTION_APPROVE && action.note.blank? + flash[:error] = l(:error_empty_note) + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def assign; end + + def assignment + if params[:commit] == l(:button_submit) && params[:dmsf_workflow_id].present? && params[:dmsf_workflow_id] != '-1' + # DMS file + if params[:dmsf_file_revision_id].present? && params[:dmsf_link_id].blank? && params[:attachment_id].blank? + revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id]) + begin + if revision + @project ||= revision.dmsf_file.project + revision.set_workflow(params[:dmsf_workflow_id], params[:action]) + revision.assign_workflow(params[:dmsf_workflow_id]) + if request.post? + if revision.save + file = DmsfFile.find_by(id: revision.dmsf_file_id) + if file + begin + file.lock! + rescue DmsfLockError => e + Rails.logger.warn e.message + end + flash[:notice] = l(:notice_successful_update) + end + else + flash[:error] = l(:error_workflow_assign) + end + end + end + rescue StandardError => e + flash[:error] = e.message + end + # DMS link (attached) + elsif params[:dmsf_link_id].present? + @dmsf_link_id = params[:dmsf_link_id] + @dmsf_workflow_id = params[:dmsf_workflow_id] + # Attachment (attached) + elsif params[:attachment_id].present? + @attachment_id = params[:attachment_id] + @dmsf_workflow_id = params[:dmsf_workflow_id] + end + end + respond_to do |format| + format.html { redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) } + format.js + end + end + + def log + if params[:dmsf_file_revision_id].present? + @revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id]) + elsif params[:dmsf_file_id].present? + dmsf_file = DmsfFile.find_by(id: params[:dmsf_file_id]) + @revision = dmsf_file.last_revision if dmsf_file + elsif params[:dmsf_link_id].present? + dmsf_link = DmsfLink.find_by(id: params[:dmsf_link_id]) + if dmsf_link + dmsf_file = dmsf_link.target_file + @revision = dmsf_file.last_revision + end + end + respond_to do |format| + format.html + format.js + end + end + + def new + @dmsf_workflow = DmsfWorkflow.new + # Reload + if params[:dmsf_workflow] && params[:dmsf_workflow][:name].present? + @dmsf_workflow.name = params[:dmsf_workflow][:name] + elsif params[:dmsf_workflow] && params[:dmsf_workflow][:id].present? + names = DmsfWorkflow.where(id: params[:dmsf_workflow][:id]).pluck(:name) + @dmsf_workflow.name = names.first + end + render layout: !request.xhr? + end + + def edit + redirect_to dmsf_workflow_path(@dmsf_workflow) + end + + def create + if params[:dmsf_workflow] + if params[:dmsf_workflow][:id].to_i.positive? + wf = DmsfWorkflow.find_by(id: params[:dmsf_workflow][:id]) + @dmsf_workflow = wf.copy_to(@project, params[:dmsf_workflow][:name]) if wf + else + @dmsf_workflow = DmsfWorkflow.new + @dmsf_workflow.name = params[:dmsf_workflow][:name] + @dmsf_workflow.project_id = @project.id if @project + @dmsf_workflow.author = User.current + @dmsf_workflow.save + end + end + if request.post? && @dmsf_workflow&.valid? + flash[:notice] = l(:notice_successful_create) + if @project + redirect_to settings_project_path(@project, tab: 'dmsf_workflow') + else + redirect_to dmsf_workflows_path + end + else + render action: 'new' + end + end + + def update + if params[:dmsf_workflow] + res = @dmsf_workflow.update(name: params[:dmsf_workflow][:name]) if params[:dmsf_workflow][:name].present? + res = @dmsf_workflow.update(status: params[:dmsf_workflow][:status]) if params[:dmsf_workflow][:status].present? + if res + flash[:notice] = l(:notice_successful_update) + if @project + redirect_to settings_project_path(@project, tab: 'dmsf_workflow') + else + redirect_to dmsf_workflows_path + end + else + flash[:error] = @dmsf_workflow.errors.full_messages.to_sentence + redirect_to dmsf_workflow_path(@dmsf_workflow) + end + else + redirect_to dmsf_workflow_path(@dmsf_workflow) + end + end + + def destroy + begin + @dmsf_workflow.destroy + flash[:notice] = l(:notice_successful_delete) + rescue StandardError + flash[:error] = l(:error_unable_delete_dmsf_workflow) + end + if @project + redirect_to settings_project_path(@project, tab: 'dmsf_workflow') + else + redirect_to dmsf_workflows_path + end + end + + def autocomplete_for_user + respond_to do |format| + format.js + end + end + + def new_step + @steps = @dmsf_workflow.dmsf_workflow_steps.select('step, MAX(name) AS name').group(:step) + respond_to do |format| + format.html + format.js + end + end + + def add_step + if request.post? + step = if params[:step] == '0' + @dmsf_workflow.dmsf_workflow_steps.collect(&:step).uniq.count + 1 + else + params[:step].to_i + end + operator = params[:commit] == l(:dmsf_and) ? DmsfWorkflowStep::OPERATOR_AND : DmsfWorkflowStep::OPERATOR_OR + user_ids = User.where(id: params[:user_ids]).ids + if user_ids.any? + user_ids.each do |user_id| + ws = DmsfWorkflowStep.new + ws.dmsf_workflow_id = @dmsf_workflow.id + ws.step = step + ws.user_id = user_id + ws.operator = operator + ws.name = params[:name] + if ws.save + @dmsf_workflow.dmsf_workflow_steps << ws + else + flash[:error] = ws.errors.full_messages.to_sentence + end + end + else + flash[:error] = l(:error_workflow_assign) + end + end + respond_to do |format| + format.html + end + end + + def remove_step + if request.delete? + DmsfWorkflowStep.where(dmsf_workflow_id: @dmsf_workflow.id, step: params[:step]).find_each do |ws| + @dmsf_workflow.dmsf_workflow_steps.delete ws + end + @dmsf_workflow.dmsf_workflow_steps.each do |ws| + n = ws.step.to_i + next unless n > params[:step].to_i + + ws.step = n - 1 + flash[:error] = l(:notice_cannot_renumber_steps) unless ws.save + end + end + redirect_back_or_default dmsf_workflow_path(@dmsf_workflow) + end + + def reorder_steps + if request.put? + if @assigned + flash[:error] = l(:error_dmsf_workflow_assigned) + else + @dmsf_workflow.reorder_steps params[:step].to_i, params[:dmsf_workflow][:position].to_i + end + end + respond_to do |format| + format.html + format.js + end + end + + def start + revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id]) + if revision + revision.set_workflow(@dmsf_workflow.id, params[:action]) + if revision.save + if Setting.notified_events.include?('dmsf_workflow_plural') + @dmsf_workflow.notify_users(@project, revision, self) + end + flash[:notice] = l(:notice_workflow_started) + else + flash[:error] = l(:notice_cannot_start_workflow) + end + end + redirect_back_or_default dmsf_folder_path(id: @project, folder_id: @folder) + end + + def update_step + # Name + if params[:dmsf_workflow].present? + index = params[:step].to_i + step = @dmsf_workflow.dmsf_workflow_steps[index] + step.name = params[:dmsf_workflow][:step_name] + if step.save + @dmsf_workflow.dmsf_workflow_steps.where(step: step.step).find_each do |s| + s.name = step.name + flash[:error] = s.errors.full_messages.to_sentence unless s.save + end + else + flash[:error] = step.errors.full_messages.to_sentence + end + end + # Operators/Assignees + if params[:operator_step].present? + params[:operator_step].each do |id, operator| + step = DmsfWorkflowStep.find_by(id: id) + next unless step + + step.operator = operator.to_i + step.user_id = params[:assignee][id] + next if step.save + + flash[:error] = step.errors.full_messages.to_sentence + Rails.logger.error step.errors.full_messages.to_sentence + end + end + redirect_to dmsf_workflow_path(@dmsf_workflow) + end + + def delete_step + step = DmsfWorkflowStep.find_by(id: params[:step]) + if step + # Safe the name + if step.name.present? + @dmsf_workflow.dmsf_workflow_steps.each do |s| + if s.step == step.step + s.name = step.name + s.save! + end + end + end + # Destroy + step.destroy + end + redirect_to dmsf_workflow_path(@dmsf_workflow) + end + + private + + def find_project + if @dmsf_workflow + if @dmsf_workflow.project # Project workflow + @project = @dmsf_workflow.project + elsif params[:dmsf_file_revision_id].present? # Global workflow + revision = DmsfFileRevision.find_by(id: params[:dmsf_file_revision_id]) + @project = revision.dmsf_file.project if revision&.dmsf_file + elsif params[:project_id].present? + @project = Project.find params[:project_id] + end + elsif params[:dmsf_workflow].present? + @project = Project.find params[:dmsf_workflow][:project_id] + elsif params[:project_id].present? + @project = Project.find params[:project_id] + elsif params[:id].present? + @project = Project.find(params[:id]) + end + rescue ActiveRecord::RecordNotFound + @project = nil + end + + def workflows_layout + @project ? 'base' : 'admin' + end + + def authorize_custom + if @project + authorize + else + require_admin + end + end + + def approver_candidates + @approving_candidates = @project ? @project.users.to_a : User.active.to_a + end + + def prevent_from_editing + # A workflow in use can be neither edited nor deleted + @assigned = DmsfFileRevision.exists?(dmsf_workflow_id: @dmsf_workflow.id) + return unless @assigned && !request.put? + + flash[:error] = l(:error_dmsf_workflow_assigned) + if @project + redirect_back_or_default settings_project_path(@project, tab: 'dmsf_workflow') + else + redirect_back_or_default dmsf_workflows_path + end + end +end diff --git a/app/errors/dmsf_access_error.rb b/app/errors/dmsf_access_error.rb new file mode 100644 index 00000000..9dba39da --- /dev/null +++ b/app/errors/dmsf_access_error.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Access error +class DmsfAccessError < StandardError + # Nothing to do +end diff --git a/app/errors/dmsf_email_max_file_size_error.rb b/app/errors/dmsf_email_max_file_size_error.rb new file mode 100644 index 00000000..ec324544 --- /dev/null +++ b/app/errors/dmsf_email_max_file_size_error.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Max file size error +class DmsfEmailMaxFileSizeError < StandardError + include Redmine::I18n + + def initialize(message = nil) + if message.present? + super + else + super(l(:error_max_email_filesize_exceeded, number: RedmineDmsf.dmsf_max_email_filesize)) + end + end +end diff --git a/app/errors/dmsf_file_not_found_error.rb b/app/errors/dmsf_file_not_found_error.rb new file mode 100644 index 00000000..002b83b9 --- /dev/null +++ b/app/errors/dmsf_file_not_found_error.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Not found error +class DmsfFileNotFoundError < StandardError + # nothing to do +end diff --git a/app/errors/dmsf_lock_error.rb b/app/errors/dmsf_lock_error.rb new file mode 100644 index 00000000..7fc2ad2c --- /dev/null +++ b/app/errors/dmsf_lock_error.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +# Lock error +class DmsfLockError < StandardError + # Nothing to do +end diff --git a/app/errors/dmsf_parent_error.rb b/app/errors/dmsf_parent_error.rb new file mode 100644 index 00000000..8b13542f --- /dev/null +++ b/app/errors/dmsf_parent_error.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Parent error +class DmsfParentError < StandardError + # Nothing to do +end diff --git a/app/errors/dmsf_zip_max_files_error.rb b/app/errors/dmsf_zip_max_files_error.rb new file mode 100644 index 00000000..6851e251 --- /dev/null +++ b/app/errors/dmsf_zip_max_files_error.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# File count exceeded +class DmsfZipMaxFilesError < StandardError + include Redmine::I18n + + def initialize(message = nil) + if message.present? + super + else + super(l(:error_max_files_exceeded, number: RedmineDmsf.dmsf_max_file_download)) + end + end +end diff --git a/app/helpers/dmsf_files_helper.rb b/app/helpers/dmsf_files_helper.rb new file mode 100644 index 00000000..2d663d4b --- /dev/null +++ b/app/helpers/dmsf_files_helper.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Files helper +module DmsfFilesHelper + def clean_wiki_text(text) + # If there is

tag, the text is moved one column to the right by Redmin's CSS. A new line causes double new line. + text.gsub('

', '') + .gsub('

', '') + .gsub("\n\n", '
') + .gsub("\n\t", '
') + end + + def render_document_content(dmsf_file, content) + if dmsf_file.markdown? + render partial: 'common/markup', locals: { markup_text_formatting: markdown_formatter, markup_text: content } + elsif dmsf_file.textile? + render partial: 'common/markup', locals: { markup_text_formatting: 'textile', markup_text: content } + else + render partial: 'common/file', locals: { content: content, filename: dmsf_file.name } + end + end +end diff --git a/app/helpers/dmsf_folder_permissions_helper.rb b/app/helpers/dmsf_folder_permissions_helper.rb new file mode 100644 index 00000000..b58db0a8 --- /dev/null +++ b/app/helpers/dmsf_folder_permissions_helper.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Folders permissions helper +module DmsfFolderPermissionsHelper + def users_checkboxes(users, inherited: false) + s = [] + id = inherited ? 'inherited_permissions[user_ids][]' : 'permissions[user_ids][]' + users.each do |user| + content = check_box_tag(id, user.id, true, disabled: inherited, id: nil) + user.name + s << content_tag(:label, content, id: "user_permission_ids_#{user.id}", class: 'inline') + end + safe_join s + end + + def render_principals_for_new_folder_permissions(users) + principals_check_box_tags 'user_ids[]', users + end +end diff --git a/app/helpers/dmsf_helper.rb b/app/helpers/dmsf_helper.rb new file mode 100644 index 00000000..bd306c62 --- /dev/null +++ b/app/helpers/dmsf_helper.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +require 'tmpdir' +require 'csv' + +# DMSF helper +module DmsfHelper + include Redmine::I18n + + def self.temp_filename(filename) + filename = sanitize_filename(filename) + timestamp = DateTime.current.strftime('%y%m%d%H%M%S') + timestamp.succ! while Rails.root.join("tmp/#{timestamp}_#{filename}").exist? + "#{timestamp}_#{filename}" + end + + 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 + just_filename.gsub!(/[^\w.\-]/, '_') + # Keep the extension if any + if !/^[a-zA-Z0-9_.\-]*$/.match?(just_filename) && just_filename =~ /(.[a-zA-Z0-9]+)$/ + extension = Regexp.last_match(1) + just_filename = Digest::SHA256.hexdigest(just_filename) << extension + end + just_filename + end + + def plugin_asset_path(plugin, asset_type, source) + File.join('/plugin_assets', plugin.to_s, asset_type, source) + end + + def render_principals_for_new_email(users) + principals_check_box_tags 'user_ids[]', users + end + + def webdav_url(project, folder) + url = ["#{Setting.protocol}:/", Setting.host_name, 'dmsf', 'webdav'] + if project + url << ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(project)) + if folder + folders = [] + while folder + folders << folder + folder = folder.dmsf_folder + end + folders.reverse_each do |f| + url << f.title + end + end + end + url << '' + url.join '/' + end + + # Downloads zipped files securely by sanitizing the params[:entry]. Characters considered unsafe + # are replaced with a underscore. The file_path is joined with File.join instead of Rails.root.join to eliminate + # the risk of overriding the absolute path (Rails.root/tmp) with file_name when given as absoulte path too. This + # makes the path double secure. + def email_entry_tmp_file_path(entry) + sanitized_entry = DmsfHelper.sanitize_filename(entry) + file_name = "#{RedmineDmsf::DmsfZip::FILE_PREFIX}#{sanitized_entry}.zip" + Rails.root.join 'tmp', file_name + end + + # Extracts the variable part of the temp file name to be used as identifier in the + # download email entries route. + def tmp_entry_identifier(zipped_content) + path = Pathname.new(zipped_content) + zipped_file = path.basename(path.extname).to_s + zipped_file.delete_prefix RedmineDmsf::DmsfZip::FILE_PREFIX + end +end diff --git a/app/helpers/dmsf_links_helper.rb b/app/helpers/dmsf_links_helper.rb new file mode 100644 index 00000000..34cd25ac --- /dev/null +++ b/app/helpers/dmsf_links_helper.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Links helper +module DmsfLinksHelper + def folder_tree_options_for_select(folder_tree, options = {}) + s = [] + folder_tree.each do |name, id| + tag_options = { value: id } + tag_options[:selected] = 'selected' if id == options[:selected] + s << content_tag('option', name, tag_options) + end + safe_join s + end + + # An integer test + def self.number?(str) + str&.match?(/\A\d+\Z/) + end + + def files_for_select(project_id, folder_id = nil) + files = [] + if DmsfLinksHelper.number?(folder_id) + folder = DmsfFolder.find_by(id: folder_id) + files = folder.dmsf_files.visible.to_a if folder + elsif project_id + project = Project.find_by(id: project_id) + files = project.dmsf_files.visible.to_a if project + end + files + end +end diff --git a/app/helpers/dmsf_queries_helper.rb b/app/helpers/dmsf_queries_helper.rb new file mode 100644 index 00000000..10ea292a --- /dev/null +++ b/app/helpers/dmsf_queries_helper.rb @@ -0,0 +1,326 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +# Queries helper +module DmsfQueriesHelper + include ApplicationHelper + + def column_value(column, item, value) + return super unless item.is_a?(DmsfFolder) + + case column.name + when :modified + val = super + case item.type + when 'file' + file = DmsfFile.find_by(id: item.id) + if !item.deleted? && file&.locked? + return content_tag(:span, val) + + link_to(sprite_icon('unlock', nil, icon_only: true, size: '12'), + unlock_dmsf_files_path(id: file, + back_url: dmsf_folder_path(id: file.project, + folder_id: file.dmsf_folder)), + title: l(:title_locked_by_user, user: file.locked_by), class: 'icon icon-unlock') + end + when 'folder' + folder = DmsfFolder.find_by(id: item.id) + if !item.deleted? && folder&.locked? + return content_tag(:span, val) + + link_to(sprite_icon('unlock', nil, icon_only: true, size: '12'), + unlock_dmsf_path(id: folder.project, + folder_id: folder.id, + back_url: dmsf_folder_path(id: folder.project, + folder_id: folder.dmsf_folder)), + title: l(:title_locked_by_user, user: folder.locked_by), class: 'icon icon-unlock') + end + end + content_tag(:span, val) + content_tag(:span, '', class: 'icon icon-none') + when :id + case item.type + when 'file' + if item.deleted? + h(value) + else + link_to h(value), dmsf_file_path(id: item.id) + end + when 'file-link' + if item.deleted? + h(item.revision_id) + else + link_to h(item.revision_id), dmsf_file_path(id: item.revision_id) + end + when 'folder' + if item.id + if item.deleted? + h(value) + else + link_to h(value), edit_dmsf_path(id: item.project_id, folder_id: item.id) + end + elsif item.deleted? + h(item.project_id) + else + link_to h(item.project_id), edit_root_dmsf_path(id: item.project_id) + end + when 'folder-link' + if item.id + if item.deleted? + h(item.revision_id) + else + link_to h(item.revision_id), edit_dmsf_path(id: item.project_id, folder_id: item.revision_id) + end + elsif item.deleted? + h(item.project_id) + else + link_to h(item.project_id), edit_root_dmsf_path(id: item.project_id) + end + when 'project' + link_to h(item.project_id), edit_root_dmsf_path(id: item.project_id) + else + h(value) + end + when :author + if value + user = User.find_by(id: value) + if user + link_to user.name, user_path(id: value) + else + super + end + else + super + end + when :title + case item.type + when 'project' + tag = h("[#{value}]") + tag = if item.project.module_enabled?(:dmsf) + link_to(sprite_icon('folder', nil, icon_only: true), dmsf_folder_path(id: item.project), + class: 'icon icon-folder') + + link_to(tag, dmsf_folder_path(id: item.project), class: 'dmsf-label') + else + sprite_icon 'folder', tag + end + unless filter_any? + path = expand_folder_dmsf_path + columns = params['c'] + if columns.present? + path << '?' + path << columns.map { |col| "c[]=#{col}" }.join('&') + end + tag = content_tag( + 'span', + '', + class: 'dmsf-expander', + onclick: "dmsfToggle(this, '#{item.id}', null,'#{escape_javascript(path)}')" + ) + tag + tag = content_tag('div', tag, class: 'row-control dmsf-row-control') + end + tag += content_tag('div', item.filename, class: 'dmsf-filename') + if item.project.watched_by?(User.current) + tag += link_to(sprite_icon('unwatch', nil, icon_only: true, size: '12'), + watch_path(object_type: 'project', object_id: item.project.id), + title: l(:button_unwatch), + method: 'delete', + class: 'icon icon-fav') + end + tag + when 'folder' + if item&.deleted? + tag = content_tag(:span, sprite_icon('folder', h(value)), class: 'icon icon-folder') + else + tag = link_to(sprite_icon('folder', nil, + icon_only: true, + css_class: item.system ? 'dmsf-system' : ''), + dmsf_folder_path(id: item.project, folder_id: item.id), class: 'icon icon-folder') + tag += link_to(h(value), dmsf_folder_path(id: item.project, folder_id: item.id), class: 'dmsf-label') + unless filter_any? + path = expand_folder_dmsf_path + columns = params['c'] + if columns.present? + path << '?' + path << columns.map { |col| "c[]=#{col}" }.join('&') + end + tag = content_tag( + 'span', + '', + class: 'dmsf-expander', + onclick: "dmsfToggle(this, '#{item.project.id}', '#{item.id}','#{escape_javascript(path)}')" + ) + tag + tag = content_tag('div', tag, class: 'row-control dmsf-row-control') + end + end + tag += content_tag('div', item.filename, class: 'dmsf-filename', title: l(:title_filename_for_download)) + if !item&.deleted? && item.watched_by?(User.current) + tag += link_to(sprite_icon('unwatch', nil, icon_only: true, size: '12'), + watch_path(object_type: 'dmsf_folder', object_id: item.id), + title: l(:button_unwatch), + method: 'delete', + class: 'icon icon-fav') + end + tag + when 'folder-link' + if item&.deleted? + tag = content_tag(:span, sprite_icon('folder', h(value)), class: 'icon icon-folder') + else + # For links, we use revision_id containing dmsf_folder.id in fact + tag = link_to(sprite_icon('folder', nil, icon_only: true, css_class: 'dmsf-gray'), + dmsf_folder_path(id: item.project, folder_id: item.revision_id), class: 'icon icon-folder') + tag += link_to(h(value), dmsf_folder_path(id: item.project, folder_id: item.revision_id), class: 'dmsf-label') + tag = content_tag('span', '', class: 'dmsf-expander') + tag unless filter_any? + end + tag + content_tag('div', item.filename, class: 'dmsf-filename', title: l(:label_target_folder)) + when 'file', 'file-link' + icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(item.filename)) + icon_class = icon_class_for_mime_type(item.filename) + if item&.deleted? + tag = content_tag(:span, sprite_icon(icon_name, h(value)), class: "icon #{icon_class}") + else + # For links, we use revision_id containing dmsf_file.id in fact + file_view_url = url_for( + { controller: :dmsf_files, action: 'view', id: item.type == 'file' ? item.id : item.revision_id } + ) + content_type = Redmine::MimeType.of(item.filename) + content_type = 'application/octet-stream' if content_type.blank? + options = { class: "dmsf-label icon #{icon_class}", + 'data-downloadurl': "#{content_type}:#{h(value)}:#{file_view_url}" } + unless previewable?(item.filename, content_type) + options[:target] = '_blank' + options[:rel] = 'noopener' + end + tag = link_to(sprite_icon(icon_name, + nil, + icon_only: true, + css_class: item.type == 'file-link' ? 'dmsf-gray' : ''), + file_view_url, + options) + options[:class] = 'dmsf-label' + tag += link_to(h(value), file_view_url, options) + tag = content_tag('span', '', class: 'dmsf-expander') + tag unless filter_any? + end + member = Member.find_by(user_id: User.current.id, project_id: item.project_id) + revision = DmsfFileRevision.find_by(id: item.customized_id) + filename = revision ? revision.formatted_name(member) : item.filename + tag += content_tag('div', filename, class: 'dmsf-filename', title: l(:title_filename_for_download)) + if (item.type == 'file') && !item&.deleted? && revision.dmsf_file&.watched_by?(User.current) + tag += link_to(sprite_icon('unwatch', nil, icon_only: true, size: '12'), + watch_path(object_type: 'dmsf_file', object_id: item.id), + title: l(:button_unwatch), + method: 'delete', + class: 'icon icon-fav') + end + tag + when 'url-link' + if item&.deleted? + tag = content_tag(:span, sprite_icon('link', h(value)), class: 'icon icon-link') + else + tag = link_to(sprite_icon('link', nil, icon_only: true, css_class: 'dmsf-gray'), + item.filename, + target: '_blank', + rel: 'noopener', + class: 'icon icon-link') + tag += link_to(h(value), item.filename, target: '_blank', rel: 'noopener') + tag = content_tag('span', '', class: 'dmsf-expander') + tag unless filter_any? + end + tag + content_tag('div', item.filename, class: 'dmsf-filename', title: l(:field_url)) + else + h(value) + end + when :size + number_to_human_size value + when :workflow + if value + if item.workflow_id && !item&.deleted? + url = if item.type == 'file' + log_dmsf_workflow_path project_id: item.project_id, id: item.workflow_id, dmsf_file_id: item.id + else + log_dmsf_workflow_path project_id: item.project_id, id: item.workflow_id, dmsf_link_id: item.id + end + text, names = DmsfWorkflow.workflow_info(item.workflow, item.workflow_id, item.customized_id) + link_to h(text), url, remote: true, title: names + else + h(DmsfWorkflow.workflow_str(value.to_i)) + end + else + super + end + when :comment + value.present? ? content_tag('div', textilizable(value), class: 'wiki') : '' + else + super + end + end + + def csv_value(column, object, value) + case column.name + when :size + ActiveSupport::NumberHelper.number_to_human_size value + when :workflow + if value + text, _names = DmsfWorkflow.workflow_info(object.workflow, object.workflow_id, object.revision_id) + text + else + super + end + when :author + if value + user = User.find_by(id: value) + if user + user.name + else + super + end + end + else + super + end + end + + def filter_any? + # :v - value, :op - operator + size = params[:op]&.keys&.size + if size + if (size == 1) && params[:op].key?('title') && params[:op]['title'] == '~' && params[:v]['title'].join.empty? + return false + end + + return true + end + false + end + + def previewable?(filename, content_type) + case content_type + when 'text/markdown', 'text/x-textile' + true + else + Redmine::MimeType.is_type?('text', filename) || Redmine::SyntaxHighlighting.filename_supported?(filename) + end + end + + def icon_class_for_mime_type(mime) + case Redmine::MimeType.of(mime) + when 'application/pdf' + 'icon-pdf' + when 'text/plain' + 'icon-txt' + else + 'icon-file' + end + end +end diff --git a/app/helpers/dmsf_upload_helper.rb b/app/helpers/dmsf_upload_helper.rb new file mode 100644 index 00000000..818aa42c --- /dev/null +++ b/app/helpers/dmsf_upload_helper.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Upload helper +module DmsfUploadHelper + include Redmine::I18n + + def self.commit_files_internal(committed_files, project, folder, controller = nil, container = nil, new_object: false) + failed_uploads = [] + files = [] + if committed_files + failed_uploads = [] + committed_files.each_value do |committed_file| + name = committed_file[:name] + new_revision = DmsfFileRevision.new + file = DmsfFile.visible.find_file_by_name(project, folder, name) + unless file + link = DmsfLink.find_link_by_file_name(project, folder, name) + file = link.target_file if link + end + if file + if file.last_revision + last_revision = file.last_revision + new_revision.source_revision = last_revision + end + else + file = DmsfFile.new + file.project_id = project.id + file.name = name + file.dmsf_folder = folder + file.notification = RedmineDmsf.dmsf_default_notifications? + end + if file.locked_for_user? + failed_uploads.push file + next + end + + new_revision.dmsf_file = file + new_revision.user = User.current + new_revision.name = name + new_revision.title = committed_file[:title] + new_revision.description = committed_file[:description] + new_revision.comment = committed_file[:comment] + new_revision.major_version = if committed_file[:version_major].present? + DmsfUploadHelper.db_version committed_file[:version_major] + else + 1 + end + new_revision.minor_version = if committed_file[:version_minor].present? + DmsfUploadHelper.db_version committed_file[:version_minor] + end + 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. + # File id is needed to properly generate revision disk filename + unless new_revision.valid? + Rails.logger.error new_revision.errors.full_messages.to_sentence + failed_uploads.push new_revision + next + end + unless file.save + Rails.logger.error file.errors.full_messages.to_sentence + failed_uploads.push file + 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 + # If the path is not present we get it from the token + if committed_file[:token].present? + 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) + file.last_revision = new_revision + files.push file + container.dmsf_file_added file if container && !new_object + Redmine::Hook.call_hook :dmsf_helper_upload_after_commit, { file: file } + rescue StandardError => e + Rails.logger.error e.message + controller.flash[:error] = e.message if controller + failed_uploads.push file + end + else + failed_uploads.push new_revision + end + # Approval workflow + next if committed_file[:workflow_id].blank? + + wf = DmsfWorkflow.find_by(id: committed_file[:workflow_id]) + next unless wf + + # Assign the workflow + new_revision.set_workflow wf.id, 'assign' + new_revision.assign_workflow wf.id + # Start the workflow + new_revision.set_workflow wf.id, 'start' + if new_revision.save + wf.notify_users project, new_revision, controller + begin + file.lock! + rescue DmsfLockError => e + Rails.logger.warn e.message + end + else + Rails.logger.error l(:error_workflow_assign) + end + end + # Notifications + begin + recipients = DmsfMailer.deliver_files_updated(project, files) + if RedmineDmsf.dmsf_display_notified_recipients? && recipients.any? + max_recipients = RedmineDmsf.dmsf_max_notification_receivers_info + to = recipients.collect { |user, _| user.name }.first(max_recipients).join(', ') + if to.present? + to << (recipients.count > max_recipients ? ',...' : '.') + controller.flash[:warning] = l(:warning_email_notifications, to: to) if controller + end + end + rescue StandardError => e + Rails.logger.error "Could not send email notifications: #{e.message}" + end + end + if failed_uploads.present? && controller + controller.flash[:warning] = l(:warning_some_files_were_not_committed, + files: failed_uploads.pluck(:name).join(', ')) + end + [files, failed_uploads] + end + + # 0..99, A..Z except O + def self.major_version_select_options + (0..99).to_a + ('A'..'Y').to_a - ['O'] + end + + # 0..999, ' ' + def self.minor_version_select_options + (0..999).to_a + [' '] + end + + class << self + alias patch_version_select_options minor_version_select_options + end + + # 1 -> 2, -1 -> -2, A -> B + def self.increase_version(version) + return nil unless version + return 1 if (version == ' ') || ((-version) == ' '.ord) + + if Integer(version) + if version >= 0 + if (version + 1) < 1000 + version + 1 + else + version + end + elsif -(version - 1) < 'Z'.ord + version - 1 + else + version + end + end + rescue StandardError + if (version.ord + 1) < 'Z'.ord + (version.ord + 1).chr + else + version + end + end + + # 1 -> 1, -65 -> A + def self.gui_version(version) + return ' ' unless version + return version if version >= 0 + + (-version).chr + end + + # 1 -> 1, A -> -65 + def self.db_version(version) + return nil if version.nil? || version == ' ' + + version.to_i if Integer(version) + rescue StandardError + version = ' ' if version.blank? + -version.ord + end +end diff --git a/app/helpers/dmsf_workflows_helper.rb b/app/helpers/dmsf_workflows_helper.rb new file mode 100644 index 00000000..82ec20e2 --- /dev/null +++ b/app/helpers/dmsf_workflows_helper.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Workflows helper +module DmsfWorkflowsHelper + def render_principals_for_new_dmsf_workflow_users(workflow, dmsf_workflow_step_assignment_id = nil, + dmsf_file_revision_id = nil) + scope = workflow.delegates(params[:q], dmsf_workflow_step_assignment_id, dmsf_file_revision_id) + principal_count = scope.count + principal_pages = Redmine::Pagination::Paginator.new principal_count, 100, params['page'] + principals = scope.offset(principal_pages.offset).limit(principal_pages.per_page).to_a + # Delegation + s = [] + s << if dmsf_workflow_step_assignment_id + content_tag( + 'div', + content_tag('div', principals_radio_button_tags('step_action', principals), + id: 'dmsf_users_for_delegate'), + class: 'objects-selection' + ) + # New step + else + content_tag( + 'div', + content_tag('div', principals_check_box_tags('user_ids[]', principals), id: 'users'), + class: 'objects-selection' + ) + end + links = pagination_links_full(principal_pages, principal_count, per_page_links: false) do |text, parameters, _| + link_to text, + autocomplete_for_user_dmsf_workflow_path(workflow, parameters.merge(q: params[:q], format: 'js')), + remote: true + end + s << content_tag('span', links, class: 'pagination') + safe_join s + end + + def dmsf_workflow_steps_options_for_select(steps) + options = [[l(:dmsf_new_step), 0]] + steps.each do |step| + options << [step.name.presence || step.step.to_s, step.step] + end + options_for_select options, 0 + end + + def dmsf_workflows_for_select(project, dmsf_workflow_id) + options = [['', -1]] + DmsfWorkflow.active.sorted.where(['project_id = ? OR project_id IS NULL', project&.id]).find_each do |wf| + options << if wf.project_id + [wf.name, wf.id] + else + ["#{wf.name} #{l(:note_global)}", wf.id] + end + end + options_for_select options, selected: dmsf_workflow_id + end + + def dmsf_all_workflows_for_select(dmsf_workflow_id) + options = [['', 0]] + DmsfWorkflow.active.sorted.find_each do |wf| + if wf.project_id + prj = Project.find_by(id: wf.project_id) + if User.current.allowed_to?(:manage_workflows, prj) + # Local approval workflows + options << if prj + ["#{wf.name} (#{prj.name})", wf.id] + else + [wf.name, wf.id] + end + end + else + # Global approval workflows + options << ["#{wf.name} #{l(:note_global)}", wf.id] + end + end + options_for_select options, selected: dmsf_workflow_id + end + + def principals_radio_button_tags(name, principals) + s = [] + principals.each do |principal| + n = principal.id * 10 + id = "principal_#{n}" + s << radio_button_tag(name, n, false, onclick: 'noteMandatory(true);', id: id) + s << content_tag(:label, h(principal), for: id) + s << tag.br + end + safe_join s + end +end diff --git a/app/models/dmsf_file.rb b/app/models/dmsf_file.rb new file mode 100644 index 00000000..14f36f97 --- /dev/null +++ b/app/models/dmsf_file.rb @@ -0,0 +1,684 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/lockable" +require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/plugin" +require 'English' + +# File +class DmsfFile < ApplicationRecord + include RedmineDmsf::Lockable + + belongs_to :project + belongs_to :dmsf_folder + belongs_to :deleted_by_user, class_name: 'User' + + has_many :dmsf_file_revisions, -> { order(created_at: :desc, id: :desc) }, dependent: :destroy, inverse_of: :dmsf_file + has_many :locks, -> { where(entity_type: 0).order(updated_at: :desc) }, + class_name: 'DmsfLock', foreign_key: 'entity_id', dependent: :destroy, inverse_of: :dmsf_file + has_many :referenced_links, -> { where target_type: DmsfFile.model_name.to_s }, + class_name: 'DmsfLink', foreign_key: 'target_id', dependent: :destroy, inverse_of: false + has_many :dmsf_public_urls, dependent: :destroy + + STATUS_DELETED = 1 + STATUS_ACTIVE = 0 + + 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 + o.dmsf_file_revisions.visible.each do |r| + key = "DmsfFile-#{o.id}-#{r.id}" + @desc = Redmine::Search.cache_store.fetch(key) + next unless @desc + + Redmine::Search.cache_store.delete key + @searched_revision = r + break + end + if @searched_revision && (@searched_revision != o.last_revision) + "#{o.name} (r#{@searched_revision.id})" + else + o.name + end + }, + description: proc { |o| + unless @desc + # Set desc to an empty string if o.description is nil + @desc = o.description.nil? ? +'' : +o.description + @desc += ' / ' if o.description.present? && o.last_revision.comment.present? + @desc += o.last_revision.comment if o.last_revision.comment.present? + end + @desc + }, + url: proc { |o| + if @searched_revision + { controller: 'dmsf_files', action: 'view', id: o.id, download: @searched_revision.id, + filename: o.name } + else + { controller: 'dmsf_files', action: 'view', id: o.id, filename: o.name } + end + }, + datetime: proc { |o| + if @searched_revision + @searched_revision.updated_at + else + o.updated_at + end + }, + author: proc { |o| + if @searched_revision + @searched_revision.user + else + o.last_revision.user + end + } + ) + acts_as_watchable + acts_as_searchable( + columns: [ + "#{table_name}.name", + "#{DmsfFileRevision.table_name}.title", + "#{DmsfFileRevision.table_name}.description", + "#{DmsfFileRevision.table_name}.comment" + ], + project_key: 'project_id', + date_column: "#{table_name}.updated_at" + ) + + before_create :default_values + before_destroy :delete_system_folder_before + after_destroy :delete_system_folder_after + + attr_writer :last_revision + + def self.previews_storage_path + Rails.root.join 'tmp/dmsf_previews' + end + + def default_values + return unless RedmineDmsf.dmsf_default_notifications? && (!dmsf_folder || !dmsf_folder.system) + + self.notification = true + end + + def initialize(*args) + super + self.watcher_user_ids = [] if new_record? + end + + def self.storage_path + path = RedmineDmsf.dmsf_storage_directory + pn = Pathname.new(path) + return pn if pn.absolute? + + Rails.root.join path + end + + def self.find_file_by_name(project, folder, name) + findn_file_by_name project&.id, folder, name + 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 + end + + def approval_allowed_zero_minor + RedmineDmsf.only_approval_zero_minor_version? ? last_revision.minor_version&.zero? : true + end + + def last_revision + unless defined?(@last_revision) + @last_revision = deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first + end + @last_revision + end + + def deleted? + deleted == STATUS_DELETED + end + + def locked_by + if lock && lock.reverse[0] + user = lock.reverse[0].user + return user == User.current ? l(:label_me) : user.name if user + end + '' + end + + def delete(commit: false) + 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)) + else + 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 + end + rescue StandardError => e + Rails.logger.error e.message + errors.add :base, e.message + false + end + end + + def restore + if dmsf_folder_id && (dmsf_folder.nil? || dmsf_folder.deleted?) + errors.add(:base, l(:error_parent_folder)) + return false + end + dmsf_file_revisions.each(&:restore) + self.deleted = STATUS_ACTIVE + self.deleted_by_user = nil + save + end + + def title + last_revision ? last_revision.title : name + end + + def description + last_revision ? last_revision.description : '' + end + + def version + last_revision ? last_revision.version : '0' + end + + def workflow + last_revision&.workflow + end + + def size + last_revision ? last_revision.size : 0 + end + + def dmsf_path + path = dmsf_folder ? dmsf_folder.dmsf_path : [] + path.push self + path + end + + def dmsf_path_str + dmsf_path.map(&:title).join('/') + end + + def notify? + notification || dmsf_folder&.notify? || (!dmsf_folder && project.dmsf_notification) + end + + def get_all_watchers(watchers) + watchers.concat notified_watchers + if dmsf_folder + watchers.concat dmsf_folder.notified_watchers + else + watchers.concat project.notified_watchers + end + end + + def notify_deactivate + self.notification = nil + save! + end + + def notify_activate + self.notification = true + save! + end + + # Returns an array of projects that current user can copy file to + def self.allowed_target_projects_on_copy + projects = [] + if User.current.admin? + projects = Project.visible.has_module('dmsf').all + elsif User.current.logged? + User.current.memberships.each do |m| + projects << m.project if m.project.module_enabled?('dmsf') && + m.roles.detect { |r| r.allowed_to?(:file_manipulation) } + end + end + projects + end + + def move_to(project, folder) + unless last_revision + errors.add :base, l(:error_at_least_one_revision_must_be_present) + Rails.logger.error l(:error_at_least_one_revision_must_be_present) + return false + end + source = "#{self.project.identifier}:#{dmsf_path_str}" + self.project = project + self.dmsf_folder = folder + new_revision = last_revision.clone + new_revision.user_id = last_revision.user_id + new_revision.workflow = nil + new_revision.dmsf_workflow_id = nil + new_revision.dmsf_workflow_assigned_by_user_id = nil + new_revision.dmsf_workflow_assigned_at = nil + new_revision.dmsf_workflow_started_by_user_id = nil + new_revision.dmsf_workflow_started_at = nil + new_revision.dmsf_file = self + new_revision.comment = l(:comment_moved_from, source: source) + new_revision.custom_values = [] + last_revision.custom_values.each do |cv| + new_revision.custom_values << CustomValue.new({ custom_field: cv.custom_field, value: cv.value }) + end + self.last_revision = new_revision + save && new_revision.save + end + + def copy_to(project, folder = nil) + copy_to_filename project, folder, name + end + + def copy_to_filename(project, folder, filename) + 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.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 + new_revision.workflow = nil + new_revision.dmsf_workflow_id = nil + new_revision.dmsf_workflow_assigned_by_user_id = nil + new_revision.dmsf_workflow_assigned_at = nil + new_revision.dmsf_workflow_started_by_user_id = nil + new_revision.dmsf_workflow_started_at = nil + wf = last_revision.dmsf_workflow + if wf && (wf.project.nil? || (wf.project.id == project.id)) + 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) + end + new_revision.comment = l(:comment_copied_from, source: "#{self.project.identifier}:#{dmsf_path_str}") + new_revision.custom_values = [] + last_revision.custom_values.each do |cv| + v = CustomValue.new + v.custom_field = cv.custom_field + v.value = cv.value + new_revision.custom_values << v + 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 + end + else + Rails.logger.error file.errors.full_messages.to_sentence + file.delete commit: true + file = nil + end + file + end + + # To fulfill searchable module expectations + def self.search(tokens, projects = nil, options = {}, user = User.current) + tokens = [] << tokens unless tokens.is_a?(Array) + projects = [] << projects if projects.is_a?(Project) + project_ids = projects.collect(&:id) if projects + limit_options = ["dmsf_files.updated_at #{options[:before] ? '<' : '>'} ?", options[:offset]] if options[:offset] + columns = if options[:titles_only] + [searchable_options[:columns][1]] + else + searchable_options[:columns] + end + + token_clauses = columns.collect { |column| "(LOWER(#{column}) LIKE ?)" } + + sql = (["(#{token_clauses.join(' OR ')})"] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + find_options = [sql, * (tokens.collect { |w| "%#{w.downcase}%" } * token_clauses.size).sort] + + project_conditions = [] + project_conditions << Project.allowed_to_condition(user, :view_dmsf_files) + project_conditions << "#{Project.table_name}.id IN (#{project_ids.join(',')})" if project_ids.present? + + scope = visible + .joins('JOIN dmsf_file_revisions ON dmsf_file_revisions.dmsf_file_id = dmsf_files.id') + .joins(:project) + scope = scope.limit(options[:limit]) if options[:limit].present? + scope = scope.where(limit_options) if limit_options.present? + scope = scope.where(project_conditions.join(' AND ')) + 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') + database = nil + begin + lang = RedmineDmsf.dmsf_stemming_lang + databasepath = File.join(RedmineDmsf.dmsf_index_database, lang) + database = Xapian::Database.new(databasepath) + rescue StandardError => e + Rails.logger.error "REDMINE_XAPIAN ERROR: Xapian database is not properly set, initiated or it's corrupted." + Rails.logger.error e.message + end + + if database + enquire = Xapian::Enquire.new(database) + + # Combine the rest of the command line arguments with spaces between + # them, so that simple queries don't have to be quoted at the shell + # level. + query_string = tokens.map { |x| x[-1, 1].eql?('*') ? x : "#{x}*" }.join(' ') + qp = Xapian::QueryParser.new + stemmer = Xapian::Stem.new(lang) + qp.stemmer = stemmer + qp.database = database + + case RedmineDmsf.dmsf_stemming_strategy + when 'STEM_NONE' + qp.stemming_strategy = Xapian::QueryParser::STEM_NONE + when 'STEM_SOME' + qp.stemming_strategy = Xapian::QueryParser::STEM_SOME + when 'STEM_ALL' + qp.stemming_strategy = Xapian::QueryParser::STEM_ALL + end + + qp.default_op = if options[:all_words] + Xapian::Query::OP_AND + else + Xapian::Query::OP_OR + end + + flags = Xapian::QueryParser::FLAG_WILDCARD + flags |= Xapian::QueryParser::FLAG_CJK_NGRAM if RedmineDmsf.dmsf_enable_cjk_ngrams? + + query = qp.parse_query(query_string, flags) + + enquire.query = query + matchset = enquire.mset(0, 1000) + + matchset&.matches&.each do |m| + docdata = m.document.data { url } + dochash = Hash[*docdata.scan(%r{(url|sample|modtime|author|type|size)=/?([^\n\]]+)}).flatten] + filename = dochash['url'] + next unless filename + + 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? + + dmsf_file = DmsfFile.visible.where(limit_options).find_by(id: id_attribute) + 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}", + dochash['sample'].force_encoding('UTF-8')) + end + break if options[:limit].present? && results.count >= options[:limit] + + results << dmsf_file + end + end + end + + [results, results.count] + end + + def self.search_result_ranks_and_ids(tokens, user = User.current, projects = nil, options = {}) + r = search(tokens, projects, options, user)[0] + r.map { |f| [f.updated_at.to_i, f.id] } + end + + def display_name + member = Member.find_by(user_id: User.current.id, project_id: project_id) + fname = formatted_name(member) + return "#{fname[0, 25]}...#{fname[-25, 25]}" if fname.length > 50 + + fname + end + + def text? + filename = last_revision&.disk_filename + Redmine::MimeType.is_type?('text', filename) || + Redmine::SyntaxHighlighting.filename_supported?(filename) + end + + def image? + Redmine::MimeType.is_type?('image', last_revision&.disk_filename) + end + + def pdf? + Redmine::MimeType.of(last_revision&.disk_filename) == 'application/pdf' + end + + def video? + Redmine::MimeType.is_type?('video', last_revision&.disk_filename) + end + + def html? + Redmine::MimeType.of(last_revision&.disk_filename) == 'text/html' + end + + def office_doc? + case File.extname(last_revision&.disk_filename) + when '.odt', '.ods', '.odp', '.odg', # LibreOffice + '.doc', '.docx', '.docm', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.pptm', # MS Office + '.rtf' # Universal + true + else + false + end + end + + def markdown? + Redmine::MimeType.of(last_revision&.disk_filename) == 'text/markdown' + end + + def textile? + Redmine::MimeType.of(last_revision&.disk_filename) == 'text/x-textile' + end + + def disposition + image? || pdf? || video? || html? ? 'inline' : 'attachment' + end + + def thumbnailable? + Redmine::Thumbnail.convert_available? && (image? || (pdf? && Redmine::Thumbnail.gs_available?)) + end + + def previewable? + office_doc? && RedmineDmsf::Preview.office_available? && size <= Setting.file_max_size_displayed.to_i.kilobyte + end + + # Deletes all previews + def self.clear_previews + Dir.glob(File.join(DmsfFile.previews_storage_path, '*.pdf')).each do |file| + File.delete file + end + end + + def pdf_preview + return '' unless previewable? + + target = File.join(DmsfFile.previews_storage_path, "#{File.basename(last_revision&.disk_file.to_s, '.*')}.pdf") + begin + RedmineDmsf::Preview.generate last_revision&.disk_file.to_s, target + 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}) + end + '' + end + end + + def text_preview(limit) + 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 + end + end + rescue StandardError => e + result = e.message + end + end + result + end + + def formatted_name(member) + if last_revision + last_revision.formatted_name(member) + else + name + end + end + + def owner?(user) + last_revision&.user == user + end + + def involved?(user) + dmsf_file_revisions.each do |file_revision| + return true if file_revision.user == user + end + false + end + + def assigned?(user) + return false unless last_revision&.dmsf_workflow + + last_revision.dmsf_workflow.next_assignments(last_revision.id).each do |assignment| + return true if assignment.user == user + end + false + end + + def custom_value(custom_field) + return nill unless last_revision + + last_revision.custom_field_values.each do |cv| + return cv if cv.custom_field == custom_field + end + 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 + + return "#{l(:label_user)} #{l('activerecord.errors.messages.invalid')}" + end + l(:title_unlock_file) + end + + def container + return unless dmsf_folder&.system && dmsf_folder.title&.match(/(^\d+)/) + + id = Regexp.last_match(1).to_i + case dmsf_folder.dmsf_folder&.title + when '.Issues' + Issue.visible.find_by id: id + end + end + + def to_s + name + end + + def delete_system_folder_before + @parent_folder = dmsf_folder + end + + def delete_system_folder_after + return unless @parent_folder&.system && @parent_folder.dmsf_files.empty? && @parent_folder.dmsf_links.empty? + + @parent_folder&.destroy + end +end diff --git a/app/models/dmsf_file_revision.rb b/app/models/dmsf_file_revision.rb new file mode 100644 index 00000000..46b31d45 --- /dev/null +++ b/app/models/dmsf_file_revision.rb @@ -0,0 +1,466 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# File revision +class DmsfFileRevision < ApplicationRecord + belongs_to :dmsf_file, inverse_of: :dmsf_file_revisions + belongs_to :source_revision, + class_name: 'DmsfFileRevision', + foreign_key: 'source_dmsf_file_revision_id', + inverse_of: false + belongs_to :user + belongs_to :deleted_by_user, class_name: 'User' + belongs_to :dmsf_workflow_started_by_user, class_name: 'User' + belongs_to :dmsf_workflow_assigned_by_user, class_name: 'User' + belongs_to :dmsf_workflow + + has_many :dmsf_file_revision_access, dependent: :destroy + has_many :dmsf_workflow_step_assignment, dependent: :destroy + + before_destroy :delete_source_revision + + STATUS_DELETED = 1 + STATUS_ACTIVE = 0 + + PATCH_VERSION = 1 + MINOR_VERSION = 2 + MAJOR_VERSION = 3 + + PROTOCOLS = { + 'application/msword' => 'ms-word', + 'application/excel' => 'ms-excel', + 'application/vnd.ms-excel' => 'ms-excel', + 'application/vnd.ms-powerpoint' => 'ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'ms-word', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'ms-excel', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ms-powerpoint', + 'application/vnd.oasis.opendocument.spreadsheet' => 'ms-excel', + 'application/vnd.oasis.opendocument.text' => 'ms-word', + 'application/vnd.oasis.opendocument.presentation' => 'ms-powerpoint', + 'application/vnd.ms-excel.sheet.macroenabled.12' => 'ms-excel' + }.freeze + + scope :visible, -> { where(deleted: STATUS_ACTIVE) } + scope :deleted, -> { where(deleted: STATUS_DELETED) } + + acts_as_customizable + acts_as_event( + title: proc { |o| + if o.source_dmsf_file_revision_id.present? + "#{l(:label_dmsf_updated)}: #{o.dmsf_file.dmsf_path_str}" + else + "#{l(:label_created)}: #{o.dmsf_file.dmsf_path_str}" + end + }, + url: proc { |o| { controller: 'dmsf_files', action: 'show', id: o.dmsf_file } }, + datetime: proc { |o| o.updated_at }, + description: proc { |o| "#{o.description}\n#{o.comment}" }, + author: proc { |o| o.user } + ) + + acts_as_activity_provider( + type: 'dmsf_file_revisions', + timestamp: "#{DmsfFileRevision.table_name}.updated_at", + author_key: "#{DmsfFileRevision.table_name}.user_id", + permission: :view_dmsf_file_revisions, + scope: proc { + DmsfFileRevision + .joins(:dmsf_file) + .joins("JOIN #{Project.table_name} ON #{Project.table_name}.id = #{DmsfFile.table_name}.project_id") + .visible + } + ) + + validates :title, presence: true + validates :title, length: { maximum: 255 } + 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 :description, length: { maximum: 1.kilobyte } + validates :size, dmsf_max_file_size: true + + def visible?(_user = nil) + deleted == STATUS_ACTIVE + end + + def project + dmsf_file&.project + end + + def folder + 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) + return false + end + if !commit && !force && (dmsf_file.dmsf_file_revisions.length <= 1) + errors.add :base, l(:error_at_least_one_revision_must_be_present) + return false + end + + if commit + destroy + else + self.deleted = DmsfFile::STATUS_DELETED + self.deleted_by_user = User.current + save + end + end + + def obsolete + if dmsf_file.locked_for_user? + errors.add :base, l(:error_file_is_locked) + return false + end + self.workflow = DmsfWorkflow::STATE_OBSOLETE + save + end + + def restore + self.deleted = DmsfFile::STATUS_ACTIVE + self.deleted_by_user = nil + save + end + + def version + DmsfFileRevision.version major_version, minor_version, patch_version + end + + def self.version(major_version, minor_version, patch_version) + return unless major_version + + ver = DmsfUploadHelper.gui_version(major_version).to_s + if minor_version + ver << ".#{DmsfUploadHelper.gui_version(minor_version)}" if -minor_version != ' '.ord + ver << ".#{DmsfUploadHelper.gui_version(patch_version)}" if patch_version.present? && (-patch_version != ' '.ord) + end + 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 + new_revision.major_version = major_version + new_revision.minor_version = minor_version + new_revision.patch_version = patch_version + new_revision.source_revision = self + new_revision.user = User.current + new_revision.name = name + new_revision.digest = digest + new_revision + end + + def workflow_str(name) + str = '' + if name && dmsf_workflow_id + names = DmsfWorkflow.where(id: dmsf_workflow_id).pluck(:name) + str = "#{names.first} - " if names.any? + end + case workflow + when DmsfWorkflow::STATE_WAITING_FOR_APPROVAL + str + l(:title_waiting_for_approval) + when DmsfWorkflow::STATE_APPROVED + str + l(:title_approved) + when DmsfWorkflow::STATE_ASSIGNED + str + l(:title_assigned) + when DmsfWorkflow::STATE_REJECTED + str + l(:title_rejected) + when DmsfWorkflow::STATE_OBSOLETE + str + l(:title_obsolete) + else + str + l(:title_none) + end + end + + def set_workflow(dmsf_workflow_id, commit) + self.dmsf_workflow_id = dmsf_workflow_id + if commit == 'start' + self.workflow = DmsfWorkflow::STATE_WAITING_FOR_APPROVAL + self.dmsf_workflow_started_by_user = User.current + self.dmsf_workflow_started_at = DateTime.current + else + self.workflow = DmsfWorkflow::STATE_ASSIGNED + self.dmsf_workflow_assigned_by_user = User.current + self.dmsf_workflow_assigned_at = DateTime.current + end + end + + def assign_workflow(dmsf_workflow_id) + wf = DmsfWorkflow.find_by(id: dmsf_workflow_id) + wf.assign(id) if wf && id + end + + def reset_workflow + self.workflow = nil + self.dmsf_workflow_id = nil + self.dmsf_workflow_assigned_by_user_id = nil + self.dmsf_workflow_assigned_at = nil + self.dmsf_workflow_started_by_user_id = nil + self.dmsf_workflow_started_at = nil + end + + def increase_version(version_to_increase) + # Patch version + self.patch_version = case version_to_increase + when PATCH_VERSION + patch_version ||= 0 + DmsfUploadHelper.increase_version patch_version + end + # Minor version + self.minor_version = case version_to_increase + when MINOR_VERSION + DmsfUploadHelper.increase_version minor_version + when MAJOR_VERSION + major_version.negative? ? -' '.ord : 0 + else + minor_version + end + # Major version + self.major_version = case version_to_increase + when MAJOR_VERSION + DmsfUploadHelper.increase_version major_version + else + major_version + end + 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 + end + + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields + def available_custom_fields + DmsfFileRevisionCustomField.all + end + + def iversion + parts = version.split '.' + parts.size == 2 ? ((parts[0].to_i * 1_000) + parts[1].to_i) : 0 + end + + def formatted_name(member) + format = if member&.dmsf_title_format.present? + member.dmsf_title_format + else + RedmineDmsf.dmsf_global_title_format + end + return name if format.blank? + + if name =~ /(.*)(\..*)$/ + filename = Regexp.last_match(1) + ext = Regexp.last_match(2) + else + filename = name + end + format2 = format + format2 = format2.sub('%t', title) + format2 = format2.sub('%f', filename) + format2 = format2.sub('%d', updated_at.strftime('%Y%m%d%H%M%S')) + format2 = format2.sub('%v', version) + format2 = format2.sub('%i', dmsf_file.id.to_s) + format2 = format2.sub('%r', id.to_s) + format2 += ext if ext + 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? + text += ' / ' if text.present? + text += comment + end + text + end + + def workflow_tooltip + tooltip = +'' + if dmsf_workflow + case workflow + when DmsfWorkflow::STATE_WAITING_FOR_APPROVAL, DmsfWorkflow::STATE_ASSIGNED + assignments = dmsf_workflow.next_assignments(id) + assignments&.each_with_index do |assignment, index| + tooltip << ', ' if index.positive? + tooltip << assignment.user.name + end + when DmsfWorkflow::STATE_APPROVED, DmsfWorkflow::STATE_REJECTED + action = DmsfWorkflowStepAction.joins(:dmsf_workflow_step_assignment) + .where(dmsf_workflow_step_assignments: { dmsf_file_revision_id: id }) + .order('dmsf_workflow_step_actions.created_at') + .last + tooltip << action.author.name if action + end + end + tooltip + end + + def protocol + @protocol ||= PROTOCOLS[mime_type.downcase] if mime_type + @protocol + end + + def delete_source_revision + DmsfFileRevision.where(source_dmsf_file_revision_id: id).find_each do |d| + d.source_revision = source_revision + d.save! + end + return unless RedmineDmsf.physical_file_delete? + + dependencies = DmsfFileRevision.where(disk_filename: disk_filename).all.size + FileUtils.rm_f(disk_file) if dependencies <= 1 + end + + def copy_custom_field_values(values, source_revision = nil) + # For a new revision we need to remember attachments' ids + if source_revision + ids = [] + source_revision.custom_field_values.each do |cv| + ids << cv.value if cv.custom_field.format.is_a?(Redmine::FieldFormat::AttachmentFormat) + end + end + # ActionParameters => Hash + h = DmsfFileRevision.params_to_hash(values) + # From a REST API call we don't get "20" => "Project" but "CustomFieldValue20" => "Project" + h.transform_keys! { |key| key.to_i.zero? && key.to_s.match(/(\d+)/) ? :Regexp.last_match(0) : key } + # Super + self.custom_field_values = h + # For a new revision we need to duplicate attachments + return unless source_revision + + i = 0 + custom_field_values.each do |cv| + next unless cv.custom_field.format.is_a?(Redmine::FieldFormat::AttachmentFormat) + + if cv.value.blank? && h[cv.custom_field.id.to_s].present? + a = Attachment.find_by(id: ids[i]) + copy = a.copy + copy.save + cv.value = copy.id + end + i += 1 + end + end + + # Converts ActionParameters to an ordinary Hash + def self.params_to_hash(params) + result = {} + return result if params.blank? + + h = params.permit!.to_hash + h.each do |key, value| + if value.is_a?(Hash) + value = value.except('blank') + _, v = value.first + # We need a symbols here + result[key] = if v&.key?('file') && v['file'].blank? + nil + else + v&.symbolize_keys + end + else + result[key] = value + end + end + result + end +end diff --git a/app/models/dmsf_file_revision_access.rb b/app/models/dmsf_file_revision_access.rb new file mode 100644 index 00000000..877f2f73 --- /dev/null +++ b/app/models/dmsf_file_revision_access.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# File revision access +class DmsfFileRevisionAccess < ApplicationRecord + belongs_to :dmsf_file_revision + belongs_to :user + + delegate :dmsf_file, to: :dmsf_file_revision, allow_nil: false + delegate :project, to: :dmsf_file, allow_nil: false + + scope :access_grouped, + lambda { + select('user_id, COUNT(*) AS count, MIN(created_at) AS first_at, MAX(created_at) AS last_at').group('user_id') + } + + DOWNLOAD_ACTION = 0 + EMAIL_ACTION = 1 + + acts_as_event( + title: proc { |ra| "#{l(:label_dmsf_downloaded)}: #{ra.dmsf_file.dmsf_path_str}" }, + url: proc { |ra| { controller: 'dmsf_files', action: 'show', id: ra.dmsf_file } }, + datetime: proc { |ra| ra.updated_at }, + description: proc { |ra| ra.dmsf_file_revision.comment }, + author: proc { |ra| ra.user } + ) + + acts_as_activity_provider( + type: 'dmsf_file_revision_accesses', + timestamp: "#{DmsfFileRevisionAccess.table_name}.updated_at", + author_key: "#{DmsfFileRevisionAccess.table_name}.user_id", + permission: :view_dmsf_file_revision_accesses, + scope: proc { + DmsfFileRevisionAccess + .joins(:dmsf_file_revision) + .joins("JOIN #{DmsfFile.table_name} ON dmsf_files.id = dmsf_file_revisions.dmsf_file_id") + .joins("JOIN #{Project.table_name} on dmsf_files.project_id = projects.id") + .where(dmsf_files: { deleted: DmsfFile::STATUS_ACTIVE }) + } + ) +end diff --git a/app/models/dmsf_file_revision_access_query.rb b/app/models/dmsf_file_revision_access_query.rb new file mode 100644 index 00000000..9b297a71 --- /dev/null +++ b/app/models/dmsf_file_revision_access_query.rb @@ -0,0 +1,80 @@ +# encode: utf-8 +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# File revision access query +class DmsfFileRevisionAccessQuery < Query + attr_accessor :revision_id + + self.queried_class = DmsfFileRevisionAccess + self.view_permission = :view_dmsf_files + + # Standard columns + self.available_columns = [ + QueryColumn.new(:user, frozen: true), + QueryColumn.new(:count, frozen: true), + QueryColumn.new(:first_at, frozen: true), + QueryColumn.new(:last_at, frozen: true) + ] + + def initialize(attributes = nil, *_args) + super(attributes) + self.sort_criteria = [] + self.filters = {} + end + + ###################################################################################################################### + # Inherited + # + + def base_scope + @scope ||= DmsfFileRevisionAccess.where(dmsf_file_revision_id: revision_id) + @scope + end + + # Returns the issue count + def access_count + base_scope + .group(:user_id) + .count.size + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid, e.message + end + + def type + 'DmsfFileRevisionAccessQuery' + end + + def available_columns + @available_columns ||= self.class.available_columns.dup + @available_columns + end + + ###################################################################################################################### + # New + + def accesses(options = {}) + base_scope + .access_grouped + .joins(:user) + .order(Arel.sql('COUNT(*) DESC')) + .limit(options[:limit]) + .offset(options[:offset]) + end +end diff --git a/app/models/dmsf_file_revision_custom_field.rb b/app/models/dmsf_file_revision_custom_field.rb new file mode 100644 index 00000000..69683ea8 --- /dev/null +++ b/app/models/dmsf_file_revision_custom_field.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Custom field +class DmsfFileRevisionCustomField < CustomField + def type_name + :menu_dmsf + end + + def compare_values?(x_value, y_value) + if x_value.is_a?(Array) && y_value.is_a?(Array) + y_value.reject!(&:empty) + return true if y_value.empty? + + x_value.reject!(&:empty?) + y_value.each do |b| + return true if x_value.include?(b) + end + false + else + x_value == y_value + end + end +end diff --git a/app/models/dmsf_folder.rb b/app/models/dmsf_folder.rb new file mode 100644 index 00000000..eb6fdeee --- /dev/null +++ b/app/models/dmsf_folder.rb @@ -0,0 +1,616 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +require "#{File.dirname(__FILE__)}/../../lib/redmine_dmsf/lockable" + +# Folder +class DmsfFolder < ApplicationRecord + include RedmineDmsf::Lockable + + belongs_to :project + belongs_to :dmsf_folder + 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_files, dependent: :destroy + has_many :folder_links, -> { where(target_type: 'DmsfFolder').order(:name) }, + 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 + has_many :url_links, -> { where(target_type: 'DmsfUrl') }, + class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder + has_many :dmsf_links, dependent: :destroy + has_many :referenced_links, -> { where(target_type: 'DmsfFolder') }, + class_name: 'DmsfLink', foreign_key: 'target_id', dependent: :destroy, inverse_of: :dmsf_folder + has_many :locks, -> { where(entity_type: 1).order(updated_at: :desc) }, + class_name: 'DmsfLock', foreign_key: 'entity_id', dependent: :destroy, inverse_of: :dmsf_folder + has_many :dmsf_folder_permissions, dependent: :destroy + + INVALID_CHARACTERS = '\[\]\/\\\?":<>#%\*' + STATUS_DELETED = 1 + STATUS_ACTIVE = 0 + AVAILABLE_COLUMNS = %w[id title size modified version workflow author description comment].freeze + DEFAULT_COLUMNS = %w[title size modified version workflow author].freeze + + def self.visible_condition(system: true) + Project.allowed_to_condition(User.current, :view_dmsf_folders) do |role, user| + if role.member? + group_ids = user.group_ids.join(',') + group_ids = -1 if group_ids.blank? + allowed = system && role.allowed_to?(:display_system_folders) ? 1 : 0 + %{ + ((dfp.object_id IS NULL) OR + (dfp.object_id = #{role.id} AND dfp.object_type = 'Role') OR + ((dfp.object_id = #{user.id} OR dfp.object_id IN (#{group_ids})) AND dfp.object_type = 'User')) AND + (#{DmsfFolder.table_name}.system = #{DmsfFolder.connection.quoted_false} OR 1 = #{allowed}) + } + end + end + end + + scope :visible, (lambda do |*args| + system = args.shift || true + joins(:project) + .joins("LEFT JOIN #{DmsfFolderPermission.table_name} dfp ON #{DmsfFolder.table_name}.id = dfp.dmsf_folder_id") + .where(deleted: STATUS_ACTIVE).where(DmsfFolder.visible_condition(system: system)).distinct + end) + scope :deleted, lambda { + joins(:project) + .joins("LEFT JOIN #{DmsfFolderPermission.table_name} dfp ON #{DmsfFolder.table_name}.id = dfp.dmsf_folder_id") + .where(deleted: STATUS_DELETED).where(DmsfFolder.visible_condition).distinct + } + scope :issystem, -> { where(system: true, deleted: STATUS_ACTIVE) } + + acts_as_customizable + acts_as_searchable columns: ["#{table_name}.title", "#{table_name}.description"], + project_key: 'project_id', + date_column: 'updated_at', + permission: :view_dmsf_files, + scope: proc { DmsfFolder.visible } + acts_as_watchable + acts_as_event title: proc { |o| o.title }, + description: proc { |o| o.description }, + url: proc { |o| { controller: 'dmsf', action: 'show', id: o.project, folder_id: o } }, + datetime: proc { |o| o.updated_at }, + author: proc { |o| o.user } + + validates :title, presence: true, 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 } + validates :dmsf_folder, dmsf_folder_parent: true, if: proc { |folder| !folder.new_record? } + + before_create :default_values + + def visible?(_user = User.current) + return DmsfFolder.visible.exists?(id: id) if respond_to?(:type) && /^folder/.match?(type) + + true + end + + def self.permissions?(folder, allow_system: true, file: false) + # Administrator? + return true if User.current&.admin? || folder.nil? + + # Permissions to the project? + # If file is true we work just with the file and not viewing the folder + return false unless file || User.current&.allowed_to?(:view_dmsf_folders, folder.project) + + # System folder? + if folder&.system + return false unless allow_system || User.current.allowed_to?(:display_system_folders, folder.project) + + return false if (folder.title != '.Issues') && !folder.issue&.visible?(User.current) + end + # Permissions to the folder? + if folder.dmsf_folder_permissions.any? + role_ids = User.current.roles_for_project(folder.project).map(&:id) + role_permission_ids = folder.dmsf_folder_permissions.roles.map(&:object_id) + return true if role_ids.intersect?(role_permission_ids) + + principal_ids = folder.dmsf_folder_permissions.users.map(&:object_id) + return true if principal_ids.include?(User.current.id) + + user_group_ids = User.current.groups.map(&:id) + principal_ids.intersect? user_group_ids + else + DmsfFolder.permissions? folder.dmsf_folder, allow_system: allow_system, file: file + end + end + + def initialize(*args) + super + self.watcher_user_ids = [] if new_record? + end + + def default_values + self.notification = true if RedmineDmsf.dmsf_default_notifications? && !system + end + + def locked_by + if lock && lock.reverse[0] + user = lock.reverse[0].user + return user == User.current ? l(:label_me) : user.name if user + end + '' + end + + def delete(commit: false) + if locked? + errors.add :base, l(:error_folder_is_locked) + return false + end + if commit + destroy + else + delete_recursively + end + end + + def delete_recursively + self.deleted = STATUS_DELETED + self.deleted_by_user = User.current + save! + dmsf_files.each(&:delete) + dmsf_links.each(&:delete) + dmsf_folders.each(&:delete_recursively) + end + + def deleted? + deleted == STATUS_DELETED + end + + def restore + if dmsf_folder&.deleted? + errors.add :base, l(:error_parent_folder) + return false + end + restore_recursively + end + + def restore_recursively + self.deleted = STATUS_ACTIVE + self.deleted_by_user = nil + save! + dmsf_files.each(&:restore) + dmsf_links.each(&:restore) + dmsf_folders.each(&:restore_recursively) + end + + def dmsf_path + folder = self + path = [] + while folder + path.unshift folder + folder = folder.dmsf_folder + end + path + end + + def dmsf_path_str + path = dmsf_path + string_path = path.map(&:title) + string_path.join '/' + end + + def notify? + notification || dmsf_folder&.notify? || (!dmsf_folder && project.dmsf_notification) + end + + def get_all_watchers(watchers) + watchers << notified_watchers + watchers << if dmsf_folder + dmsf_folder.notified_watchers + else + project.notified_watchers + end + end + + def notify_deactivate + self.notification = nil + save! + end + + def notify_activate + self.notification = true + save! + end + + def self.directory_tree(project) + tree = [[l(:link_documents), nil]] + project = Project.find(project) unless project.is_a?(Project) + folders = project.dmsf_folders.visible.to_a + folders.delete_if(&:locked_for_user?) + folders.each do |folder| + tree.push ["...#{folder.title}", folder.id] + DmsfFolder.directory_subtree tree, folder, 2 + end + tree + end + + def folder_tree + tree = [[title, id]] + DmsfFolder.directory_subtree tree, self, 1 + tree + end + + def self.file_list(files) + options = [] + options.push ['', nil, { label: 'none' }] + files.each do |f| + options.push [f.title, f.id] + end + options + end + + # Returns an array of projects that current user can copy folder to + def self.allowed_target_projects_on_copy + projects = [] + if User.current.admin? + projects = Project.visible.has_module('dmsf').all + elsif User.current.logged? + User.current.memberships.each do |m| + if m.project.module_enabled?('dmsf') && + m.roles.detect { |r| r.allowed_to?(:folder_manipulation) && r.allowed_to?(:file_manipulation) } + projects << m.project + end + end + end + projects + end + + def move_to(target_project, target_folder) + if project != target_project + dmsf_files.visible.find_each do |f| + f.move_to target_project, f.dmsf_folder + end + dmsf_folders.visible.find_each do |s| + s.move_to target_project, s.dmsf_folder + end + dmsf_links.visible.find_each do |l| + l.move_to target_project, l.dmsf_folder + end + self.project = target_project + end + self.dmsf_folder = target_folder + save + end + + def copy_to(project, folder, copy_files: true) + new_folder = DmsfFolder.new + new_folder.dmsf_folder = folder + new_folder.project = folder ? folder.project : project + new_folder.title = title + new_folder.description = description + new_folder.user = User.current + new_folder.custom_values = [] + new_folder.custom_field_values = + custom_field_values.each_with_object({}) do |v, h| + h[v.custom_field_id] = v.value + end + unless new_folder.save + Rails.logger.error new_folder.errors.full_messages.to_sentence + return new_folder + end + if copy_files + dmsf_files.visible.find_each do |f| + f.copy_to project, new_folder + end + dmsf_links.visible.find_each do |l| + l.copy_to project, new_folder + end + end + dmsf_folders.visible.find_each do |s| + s.copy_to project, new_folder, copy_files: copy_files + end + dmsf_folder_permissions.find_each do |p| + p.copy_to new_folder + end + new_folder + end + + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields + def available_custom_fields + DmsfFileRevisionCustomField.all + end + + def custom_values + # We need to response with DmsfFileRevision custom values if DmsfFolder just covers files in the union of the main + # view + if respond_to?(:type) && /^file/.match?(type) + return CustomValue.where(customized_type: 'DmsfFileRevision', customized_id: revision_id) + end + + super + end + + def modified + last_update = updated_at + time = DmsfFolder.where(['project_id = ? AND dmsf_folder_id = ? AND updated_at > ?', project_id, id, last_update]) + .maximum(:updated_at) + last_update = time if time + time = DmsfFile.where(['project_id = ? AND dmsf_folder_id = ? AND updated_at > ?', project_id, id, last_update]) + .maximum(:updated_at) + last_update = time if time + time = DmsfLink.where(['project_id = ? AND dmsf_folder_id = ? AND updated_at > ?', project_id, id, last_update]) + .maximum(:updated_at) + last_update = time if time + last_update + end + + # Number of items in the folder + def items + dmsf_folders.visible.where(project_id: project_id).all.size + + dmsf_files.visible.where(project_id: project_id).all.size + + dmsf_links.visible.where(project_id: project_id).all.size + end + + def self.column_on?(column) + RedmineDmsf.dmsf_columns.include? column + end + + def custom_value(custom_field) + custom_field_values.each do |cv| + return cv if cv.custom_field == custom_field + end + + nil + end + + def self.get_column_position(column) + dmsf_columns = RedmineDmsf.dmsf_columns + pos = 0 + # 0 - checkbox + # 1 - id + if dmsf_columns.include?('id') + pos += 1 + return pos if column == 'id' + elsif column == 'id' + return nil + end + + # 2 - title + if dmsf_columns.include?('title') + pos += 1 + return pos if column == 'title' + elsif column == 'title' + return nil + end + + # 3 - size + if dmsf_columns.include?('size') + pos += 1 + return pos if column == 'size' + elsif column == 'size' + return nil + end + + # 4 - modified + if dmsf_columns.include?('modified') + pos += 1 + return pos if column == 'modified' + elsif column == 'modified' + return nil + end + + # 5 - version + if dmsf_columns.include?('version') + pos += 1 + return pos if column == 'version' + elsif column == 'version' + return nil + end + + # 6 - workflow + if dmsf_columns.include?('workflow') + pos += 1 + return pos if column == 'workflow' + elsif column == 'workflow' + return nil + end + + # 7 - author + if dmsf_columns.include?('author') + pos += 1 + return pos if column == 'author' + elsif column == 'author' + return nil + end + + # 9 - custom fields + DmsfFileRevisionCustomField.visible.each do |c| + pos += 1 if DmsfFolder.column_on?(c.name) + end + # 8 - commands + pos += 1 + return pos if column == 'commands' + + # 9 - (position) + pos += 1 + return pos if column == 'position' + + # 10 - (size calculated) + pos += 1 + return pos if column == 'size_calculated' + + # 11 - (modified calculated) + pos += 1 + return pos if column == 'modified_calculated' + + # 12 - (version calculated) + pos += 1 + return pos if column == 'version_calculated' + + # 13 - (clear title) + pos += 1 + return pos if column == 'clear_title' + + nil + end + + def issue + if @issue.nil? && system + issue_id = title.to_i + @issue = Issue.find_by(id: issue_id) if issue_id.positive? + end + @issue + end + + def update_from_params(params) + # Attributes + self.title = params[:dmsf_folder][:title].scrub.strip + self.description = params[:dmsf_folder][:description].scrub.strip + self.dmsf_folder_id = params[:parent_id].presence || params[:dmsf_folder][:dmsf_folder_id] + self.system = params[:dmsf_folder][:system].present? + # Custom fields + if params[:dmsf_folder][:custom_field_values].present? + i = 0 + params[:dmsf_folder][:custom_field_values].each do |param| + custom_field_values[i].value = param[1] + i += 1 + end + end + # Permissions + dmsf_folder_permissions.delete_all + if params[:permissions] + params[:permissions][:role_ids]&.each do |role_id| + permission = DmsfFolderPermission.new + permission.object_id = role_id + permission.object_type = Role.model_name.to_s + dmsf_folder_permissions << permission + end + params[:permissions][:user_ids]&.each do |user_id| + permission = DmsfFolderPermission.new + permission.object_id = user_id + permission.object_type = User.model_name.to_s + dmsf_folder_permissions << permission + end + end + # Save + save + end + + ALL_INVALID_CHARACTERS = /[#{INVALID_CHARACTERS}]/ + def self.get_valid_title(title) + # 1. Invalid characters are replaced with dots. + # 2. Two or more dots in a row are replaced with a single dot. + # 3. Windows' WebClient does not like a dot at the end. + title.scrub.gsub(ALL_INVALID_CHARACTERS, '.').gsub(/\.{2,}/, '.').chomp('.') + end + + def permission_for_role(role) + dmsf_folder_permissions.roles.exists? object_id: role.id + end + + def permissions_users + Principal.active.where id: dmsf_folder_permissions.users.map(&:object_id) + end + + def inherited_permissions_from + return nil unless dmsf_folder + + if dmsf_folder.dmsf_folder_permissions.any? + dmsf_folder + else + dmsf_folder.inherited_permissions_from + end + end + + def self.visible_folders(folders, project) + allowed = RedmineDmsf.dmsf_act_as_attachable? && + (project.dmsf_act_as_attachable == Project::ATTACHABLE_DMS_AND_ATTACHMENTS) && + User.current.allowed_to?(:display_system_folders, project) + folders.reject do |folder| + if folder.system + if allowed + issue_id = folder.title.to_i + if issue_id.positive? + issue = Issue.find_by(id: issue_id) + !issue&.visible? User.current + else + false + end + else + true + end + else + false + end + end + end + + def css_classes(trash) + classes = [] + if trash + if system + classes << 'dmsf-system' + else + classes << 'hascontextmenu' + classes << 'dmsf-gray' if type.match?(/link$/) + end + else + classes << 'dmsf-tree' + if %w[folder project].include?(type) + classes << 'dmsf-collapsed' + classes << 'dmsf-not-loaded' + else + classes << 'dmsf-child' + end + if system + classes << 'dmsf-system' + else + classes << 'hascontextmenu' + classes << 'dmsf-draggable' if type != 'project' + classes << 'dmsf-droppable' if %w[project folder].include? type + classes << 'dmsf-gray' if type.match?(/link$/) + end + end + classes.join ' ' + end + + def empty? + !(dmsf_folders.visible.exists? || dmsf_files.visible.exists? || dmsf_links.visible.exists?) + end + + def to_s + title + end + + # Check whether any child folder is equal to the folder + def any_child?(folder) + dmsf_folders.each do |child| + return true if child == folder + + child.any_child? folder + end + false + end + + class << self + def directory_subtree(tree, folder, level) + folders = folder.dmsf_folders.visible.to_a + folders.delete_if(&:locked_for_user?) + folders.each do |subfolder| + tree.push ["#{'...' * level}#{subfolder.title}", subfolder.id] + DmsfFolder.directory_subtree tree, subfolder, level + 1 + end + end + end +end diff --git a/app/models/dmsf_folder_permission.rb b/app/models/dmsf_folder_permission.rb new file mode 100644 index 00000000..51986871 --- /dev/null +++ b/app/models/dmsf_folder_permission.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Folder permission +class DmsfFolderPermission < ApplicationRecord + belongs_to :dmsf_folder + + scope :users, -> { where(object_type: User.model_name.to_s) } + scope :roles, -> { where(object_type: Role.model_name.to_s) } + + def copy_to(folder) + permission = DmsfFolderPermission.new + permission.dmsf_folder_id = folder.id + permission.object_id = object_id + permission.object_type = object_type + permission.save! + permission + end +end diff --git a/app/models/dmsf_link.rb b/app/models/dmsf_link.rb new file mode 100644 index 00000000..3ed1789b --- /dev/null +++ b/app/models/dmsf_link.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Link class +class DmsfLink < ApplicationRecord + include ActiveModel::Validations + + belongs_to :project + belongs_to :dmsf_folder + belongs_to :deleted_by_user, class_name: 'User' + belongs_to :user + + validates :name, presence: true, length: { maximum: 255 } + # 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 } + validates :external_url, dmsf_url: true + + STATUS_DELETED = 1 + STATUS_ACTIVE = 0 + + scope :visible, -> { where(deleted: STATUS_ACTIVE) } + scope :deleted, -> { where(deleted: STATUS_DELETED) } + + before_destroy :delete_system_folder_before + after_destroy :delete_system_folder_after + + def target_folder_id + if target_type == DmsfFolder.model_name.to_s + target_id + else + dmsf_folder_ids = DmsfFile.where(id: target_id).pluck(:dmsf_folder_id) + dmsf_folder_ids.first + end + end + + def target_folder + @target_folder = DmsfFolder.find_by(id: target_folder_id) if @target_folder.nil? && target_folder_id + @target_folder + end + + def target_file_id + target_id if target_type == DmsfFile.model_name.to_s + end + + def target_file + @target_file = DmsfFile.find_by(id: target_file_id) if @target_file.nil? && target_file_id + @target_file + end + + def target_project + @target_project ||= Project.find_by(id: target_project_id) + @target_project + end + + def title + name + end + + def self.find_link_by_file_name(project, folder, filename) + links = DmsfLink.where(project_id: project.id, + dmsf_folder_id: folder&.id, + target_type: DmsfFile.model_name.to_s).visible.all + links.each do |link| + return link if link&.target_file&.name == filename + end + nil + end + + def path + path = if target_type == DmsfFile.model_name.to_s && target_file + target_file.dmsf_path.map { |element| element.is_a?(DmsfFile) ? element.display_name : element.title } + .join('/') + else + target_folder ? target_folder.dmsf_path_str : +'' + end + path.insert(0, "#{target_project.name}:") if project_id != target_project_id + return "#{path[0, 25]}...#{path[-25, 25]}" if path && path.length > 50 + + path + end + + def move_to(target_project, target_folder) + self.project = target_project + self.dmsf_folder = target_folder + save + end + + def copy_to(project, folder) + link = DmsfLink.new + link.target_project_id = target_project_id + link.target_id = target_id + link.target_type = target_type + link.name = name + link.external_url = external_url + link.project_id = project.id + link.dmsf_folder_id = folder&.id + link.user = User.current + link.save! + link + end + + def delete(commit: false) + if commit + destroy + else + self.deleted = STATUS_DELETED + self.deleted_by_user = User.current + save validate: false + end + end + + def restore + if dmsf_folder_id && (dmsf_folder.nil? || dmsf_folder.deleted?) + errors.add :base, l(:error_parent_folder) + return false + end + + self.deleted = STATUS_ACTIVE + self.deleted_by_user = nil + save validate: false + end + + def delete_system_folder_before + @parent_folder = dmsf_folder + end + + def delete_system_folder_after + return unless @parent_folder&.system && @parent_folder.dmsf_files.empty? && @parent_folder.dmsf_links.empty? + + @parent_folder&.destroy + end +end diff --git a/app/models/dmsf_lock.rb b/app/models/dmsf_lock.rb new file mode 100644 index 00000000..f1c51a87 --- /dev/null +++ b/app/models/dmsf_lock.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +require 'simple_enum' + +# Lock +class DmsfLock < ApplicationRecord + before_create :generate_uuid + belongs_to :dmsf_file, foreign_key: 'entity_id', inverse_of: :locks + belongs_to :dmsf_folder, foreign_key: 'entity_id', inverse_of: :locks + belongs_to :user + + # At the moment apparently we're only supporting a write lock? + as_enum :lock_type, %i[type_write type_other] + as_enum :lock_scope, %i[scope_exclusive scope_shared] + + # We really loosely bind the value in the belongs_to above + # here we just ensure the data internal to the model is correct + # to ensure everything lists fine - it's the same as a join + # just without running the join in the first place + def dmsf_file + entity_type.zero? ? super : nil + end + + # See the file, exact same scenario + def dmsf_folder + entity_type == 1 ? super : nil + end + + def expired? + expires_at && (expires_at <= Time.current) + end + + def generate_uuid + self.uuid = UUIDTools::UUID.random_create.to_s + end + + # Let's allow our UUID to be searchable + def self.find(*args) + if args&.first.is_a?(String) && !args.first.match(/^\d*$/) + lock = find_by_uuid(*args) + raise ActiveRecord::RecordNotFound, "Couldn't find lock with uuid = #{args.first}" if lock.nil? + + lock + else + super + end + end + + def self.find_by_param(...) + find(...) + end +end diff --git a/app/models/dmsf_mailer.rb b/app/models/dmsf_mailer.rb new file mode 100644 index 00000000..0884fe95 --- /dev/null +++ b/app/models/dmsf_mailer.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +require 'mailer' + +# Mailer +class DmsfMailer < Mailer + layout 'mailer' + + def self.deliver_files_updated(project, files) + hash = {} + files.each do |file| + users = get_notify_users(project, file) + users.each do |user| + (hash[user] ||= []) << file + end + end + hash.each do |user, docs| + files_updated(user, project, docs).deliver_later + end + end + + def files_updated(user, project, files) + redmine_headers 'Project' => project.identifier if project + @files = files + @project = project + @author = files.first.last_revision.user if files.first.last_revision + @author ||= User.anonymous + message_id project + set_language_if_valid user.language + mail to: user, subject: "[#{@project.name} - #{l(:menu_dmsf)}] #{l(:text_email_doc_updated_subject)}" + end + + def self.deliver_files_deleted(project, files) + hash = {} + files.each do |file| + users = get_notify_users(project, file) + users.each do |user| + (hash[user] ||= []) << file + end + end + hash.each do |user, docs| + files_deleted(user, project, docs).deliver_later + end + end + + def files_deleted(user, project, files) + redmine_headers 'Project' => project.identifier if project + @files = files + @project = project + @author = files.first.deleted_by_user + @author ||= User.anonymous + message_id project + set_language_if_valid user.language + mail to: user, subject: "[#{@project.name} - #{l(:menu_dmsf)}] #{l(:text_email_doc_deleted_subject)}" + end + + def self.deliver_files_downloaded(project, files, remote_ip) + hash = {} + files.each do |file| + users = get_notify_users(project, file) + users.each do |user| + (hash[user] ||= []) << file if user.pref.receive_download_notification == '1' + end + end + hash.each do |user, docs| + files_downloaded(user, project, docs, remote_ip).deliver_later + end + end + + def files_downloaded(user, project, files, remote_ip) + redmine_headers 'Project' => project.identifier if project + @files = files + @project = project + @author = User.current + @remote_ip = remote_ip + message_id project + set_language_if_valid user.language + mail to: user, subject: "[#{@project.name} - #{l(:menu_dmsf)}] #{l(:text_email_doc_downloaded_subject)}" + end + + def self.deliver_send_documents(project, email_params, author) + send_documents(User.current, project, email_params, author).deliver_now + end + + def send_documents(_, project, email_params, author) + redmine_headers 'Project' => project.identifier if project + @body = email_params[:body] + @links_only = email_params[:links_only] == '1' + @public_urls = email_params[:public_urls] == '1' + @expired_at = email_params[:expired_at] + @folders = email_params[:folders] + @files = email_params[:files] + @author = author + unless @links_only + if File.exist?(email_params[:zipped_content]) + zipped_content_data = File.binread(email_params[:zipped_content]) + attachments['Documents.zip'] = { content_type: 'application/zip', content: zipped_content_data } + else + Rails.logger.error "Cannot attach #{email_params[:zipped_content]}, it doesn't exist." + end + end + skip_no_self_notified = false + begin + # We need to switch off no_self_notified temporarily otherwise the email won't be sent + if (author == User.current) && author.pref.no_self_notified + author.pref.no_self_notified = false + skip_no_self_notified = true + end + res = mail(to: email_params[:to], cc: email_params[:cc], subject: email_params[:subject], + 'From' => email_params[:from], 'Reply-To' => email_params[:reply_to]) + ensure + author.pref.no_self_notified = true if skip_no_self_notified + end + res + end + + def self.deliver_workflow_notification(users, workflow, revision, subject_id, text1_id, text2_id, notice = nil, + step = nil) + step_name = step&.name.present? ? step.name : step&.step + users.each do |user| + workflow_notification(user, workflow, revision, subject_id.to_s, text1_id.to_s, text2_id.to_s, notice, + step_name).deliver_now + end + end + + def workflow_notification(user, workflow, revision, subject_id, text1_id, text2_id, notice, step_name) + return unless user && workflow && revision + + if revision.dmsf_file&.project + @project = revision.dmsf_file.project + redmine_headers 'Project' => @project.identifier + end + set_language_if_valid user.language + @user = user + message_id workflow + @workflow = workflow + @revision = revision + @text1 = l(text1_id, name: workflow.name, filename: revision.dmsf_file.name, notice: notice, stepname: step_name) + @text2 = l(text2_id) + @notice = notice + @author = revision.dmsf_workflow_assigned_by_user + @author ||= User.anonymous + skip_no_self_notified = false + begin + # We need to switch off no_self_notified temporarily otherwise the email won't be sent + if (@author == user) && @author.pref.no_self_notified + @author.pref.no_self_notified = false + skip_no_self_notified = true + end + mail to: user, + subject: + "[#{@project.name} - #{l(:field_label_dmsf_workflow)}] #{@workflow.name} #{l(subject_id)} #{step_name}" + ensure + @author.pref.no_self_notified = true if skip_no_self_notified + end + end + + # force_notification = true => approval workflow's notifications + def self.get_notify_users(project, file, force_notification: false) + return [] unless project.active? + + # Notifications + if (force_notification && Setting.notified_events.include?('dmsf_workflow_plural')) || + (Setting.notified_events.include?('dmsf_legacy_notifications') && file&.notify?) + notify_members = project.members.active.select do |notify_member| + notify_user = notify_member.user + if notify_user == User.current && notify_user.pref.no_self_notified + false + elsif notify_member.dmsf_mail_notification.nil? + case notify_user.mail_notification + when 'all' + true + when 'selected' + notify_member.mail_notification? + when 'only_my_events' + file.involved?(notify_user) || file.assigned?(notify_user) + when 'only_owner' + file.owner? notify_user + when 'only_assigned' + file.assigned? notify_user + else + false + end + else + notify_member.dmsf_mail_notification + end + end + users = notify_members.collect(&:user) + else + users = [] + end + # Watchers + watchers = [] + file&.get_all_watchers(watchers) + users.concat watchers + users.delete(User.current) if User.current&.pref&.no_self_notified + users.uniq + end +end diff --git a/app/models/dmsf_public_url.rb b/app/models/dmsf_public_url.rb new file mode 100644 index 00000000..4b6bbbdb --- /dev/null +++ b/app/models/dmsf_public_url.rb @@ -0,0 +1,35 @@ +# encode: utf-8 +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Public URL +class DmsfPublicUrl < ApplicationRecord + include ActiveModel::Validations + + belongs_to :dmsf_file + belongs_to :user + + before_save :generate_unique_token + + private + + def generate_unique_token + self.token ||= SecureRandom.hex(16) + end +end diff --git a/app/models/dmsf_query.rb b/app/models/dmsf_query.rb new file mode 100644 index 00000000..dea55d78 --- /dev/null +++ b/app/models/dmsf_query.rb @@ -0,0 +1,586 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Query +class DmsfQuery < Query + attr_accessor :dmsf_folder_id, :deleted, :sub_projects + + self.queried_class = DmsfFolder + self.view_permission = :view_dmsf_files + + # Standard columns + self.available_columns = [ + QueryColumn.new(:id, sortable: 'id', caption: :label_column_id), + DmsfQueryTitleColumn.new(:title, sortable: 'title', frozen: true, caption: :label_column_title), + QueryColumn.new(:size, sortable: 'size', caption: :label_column_size), + DmsfQueryModifiedColumn.new(:modified, sortable: 'updated', caption: :label_column_modified), + DmsfQueryVersionColumn.new(:version, + sortable: %(major_version minor_version patch_version), + caption: :label_column_version), + QueryColumn.new(:workflow, sortable: 'workflow', caption: :label_column_workflow), + QueryColumn.new(:author, sortable: %(firstname lastname), caption: :label_column_author), + QueryColumn.new(:description, sortable: 'description', caption: :label_column_description), + QueryColumn.new(:comment, sortable: 'comment', caption: :label_column_comment) + ] + + def initialize(attributes = nil, *_args) + super(attributes) + self.sort_criteria = [] + self.filters ||= { 'title' => { operator: '~', values: [''] } } + self.dmsf_folder_id = nil + self.deleted = false + self.sub_projects = false + end + + ###################################################################################################################### + # Inherited + # + + def available_columns + unless @available_columns + @available_columns = self.class.available_columns.dup + @available_columns += DmsfFileRevisionCustomField.visible.collect do |cf| + QueryCustomFieldColumn.new(cf) + end + end + @available_columns + end + + def groupable_columns + # TODO: Implement grouping, then remove this method. + [] + end + + def default_columns_names + unless @default_column_names + @default_column_names = [] + columns = available_columns + columns&.each do |column| + name = if column.is_a?(QueryCustomFieldColumn) + column.custom_field.attributes['name'] + else + column.name.to_s + end + @default_column_names << column.name if DmsfFolder.column_on?(name) + end + end + @default_column_names + end + + def default_sort_criteria + [%w[title ASC]] + end + + def base_scope + @scope ||= [dmsf_folders_scope, dmsf_folder_links_scope, dmsf_projects_scope, dmsf_files_scope, + dmsf_file_links_scope, dmsf_url_links_scope].compact.inject(:union_all) + @scope + end + + # Returns the count of all items + def dmsf_count + # We cannot use this due to the permissions + # base_scope.where(statement).count + dmsf_nodes.size + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid, e.message + end + + def initialize_available_filters + add_available_filter 'author', type: :list, values: -> { author_values } + add_available_filter 'title', type: :text + add_available_filter 'updated', type: :date_past + add_available_filter 'locked', type: :list, values: [[l(:general_text_yes), '1'], [l(:general_text_no), '0']] + add_available_filter 'workflow', type: :list, values: [ + [l(:title_waiting_for_approval), '1'], + [l(:title_approved), '2'], + [l(:title_assigned), '3'], + [l(:title_rejected), '4'], + [l(:title_obsolete), '5'] + ] + add_custom_fields_filters DmsfFileRevisionCustomField.visible + end + + def statement + unless @statement + filters_clauses = [] + filters.each_key do |field| + v = values_for(field).clone + next if v.blank? + + operator = operator_for(field) + case field + when 'author' + v.push(User.current.id.to_s) if v.delete('me') + when 'title' + next if (operator == '~') && v.join.empty? + end + if field =~ /cf_(\d+)$/ + # custom field + available_filters # Initialize available filters #1380 + sql_cf = +sql_for_custom_field(field, operator, v, Regexp.last_match(1)) + # This is what we get + # SELECT ct.id FROM dmsf_folders ct LEFT OUTER JOIN custom_values ON custom_values.customized_type='DmsfFolder' AND custom_values.customized_id=ct.id AND custom_values.custom_field_id=78 WHERE dmsf_folders.id = ct.id AND (custom_values.value IN ('A')) AND (1=1)) + # This is what we need + # SELECT customized_id FROM custom_values WHERE customized_type=dmsf_folder.customized_type AND custom_values.customized_id=dmsf_folders.customized_id AND custom_field_id=78 AND custom_values.value IN ('A'))) + sql_cf.gsub! ' AND (1=1)', '' + sql_cf.gsub!( + "SELECT ct.id FROM dmsf_folders ct LEFT OUTER JOIN custom_values ON custom_values.customized_type='DmsfFolder' AND custom_values.customized_id=ct.id AND custom_values.custom_field_id=", + 'SELECT custom_values.customized_id FROM custom_values WHERE custom_values.customized_type=dmsf_folders.customized_type AND custom_values.customized_id=dmsf_folders.customized_id AND custom_values.custom_field_id=' + ) + sql_cf.gsub! 'WHERE dmsf_folders.id = ct.id AND (', 'AND ' + sql_cf.gsub!(/\)$/, '') + filters_clauses << sql_cf + else + filters_clauses << "(#{sql_for_field(field, operator, v, queried_table_name, field)})" + end + end + filters_clauses.compact_blank! + @statement = filters_clauses.any? ? filters_clauses.join(' AND ') : nil + end + @statement + end + + def validate_query_filters + # Skip validation for empty title (default filter) + filter = filters.delete('title') + super + # Add it back + filters['title'] = filter if filter + end + + def columns + cols = super + # Just move the optional column Id to the beginning as it isn't frozen + id_index = cols.index { |col| col.name == :id } + if id_index == 1 + id_col = cols.delete_at(id_index) + cols.insert 0, id_col + end + cols + end + + ###################################################################################################################### + # New + + def dmsf_nodes(options = {}) + order_option = ['sort', group_by_sort_order, options[:order] || sort_clause&.first].flatten.compact_blank + if order_option.size > 1 + DmsfFileRevisionCustomField.visible.pluck(:id).each do |id| + order_option[1].gsub! "cf_#{id}.value", "cf_#{id}" + end + if order_option[1] =~ /^(firstname|major_version),? (lastname|minor_version)( patch_version)? (DESC|ASC)$/ + order_option[1] = if Regexp.last_match(3).present? + "#{Regexp.last_match(1)} #{Regexp.last_match(4)}, #{Regexp.last_match(2)} + #{Regexp.last_match(4)}, #{Regexp.last_match(3)} #{Regexp.last_match(4)}" + else + "#{Regexp.last_match(1)} #{Regexp.last_match(4)}, #{Regexp.last_match(2)} + #{Regexp.last_match(4)}" + end + end + end + items = case ActiveRecord::Base.connection.adapter_name.downcase + when /sqlserver/i + # This is just a workaround for #1352. + # limit and offset cause an error in case of MS SQL + base_scope.where(statement).order order_option + else + base_scope.where(statement).order(order_option).limit(options[:limit]).offset options[:offset] + end.to_a + fo = filters_on? + items.delete_if do |item| + case item.type + when 'project' + prj = Project.find_by(id: item.id) + !prj&.dmsf_available? + when 'folder' + dmsf_folder = DmsfFolder.find_by(id: item.id) + !DmsfFolder.permissions?(dmsf_folder, allow_system: false) + when 'file' + if fo + dmsf_file = DmsfFile.find_by(id: item.id) + if dmsf_file.dmsf_folder + !DmsfFolder.permissions?(dmsf_file.dmsf_folder, allow_system: false) + else + !dmsf_file.project.dmsf_available? + end + else + false + end + when /link$/ + if fo + dmsf_link = DmsfLink.find_by(id: item.id) + if dmsf_link.dmsf_folder + !dmsf_link.dmsf_folder.visible? || !DmsfFolder.permissions?(dmsf_link.dmsf_folder, allow_system: false) + else + !dmsf_link.project&.dmsf_available? + end + else + false + end + end + end + items + end + + def extra_columns + [] + end + + def self.default(project = nil, user = User.current) + # User's default + if user&.logged? && (query_id = user.pref.default_dmsf_query).present? + query = find_by(id: query_id) + return query if query&.visible? + end + + # Project's default + project = project[:project] if project.is_a?(Hash) + query = project&.default_dmsf_query + return query if query&.visibility == VISIBILITY_PUBLIC + + # Global default + query = find_by(id: RedmineDmsf.dmsf_default_query) + return query if query&.visibility == VISIBILITY_PUBLIC + + nil + end + + private + + def filters_on? + filters.each_key do |field| + return true if values_for(field).any?(&:present?) + end + false + end + + def sub_query + case ActiveRecord::Base.connection.adapter_name.downcase + when /sqlserver/i + 'dmsf_file_revisions.id = (SELECT TOP 1 r.id FROM dmsf_file_revisions r + WHERE r.created_at = (SELECT MAX(created_at) FROM dmsf_file_revisions rr WHERE rr.dmsf_file_id = dmsf_files.id) + AND r.dmsf_file_id = dmsf_files.id ORDER BY id DESC)' + else + 'dmsf_file_revisions.id = (SELECT r.id FROM dmsf_file_revisions r WHERE r.created_at = (SELECT MAX(created_at) + FROM dmsf_file_revisions rr WHERE rr.dmsf_file_id = dmsf_files.id) AND r.dmsf_file_id = dmsf_files.id ORDER BY id + DESC LIMIT 1)' + end + end + + def integer_type + if Redmine::Database.mysql? + ActiveRecord::Base.connection.type_to_sql(:signed) + else + ActiveRecord::Base.connection.type_to_sql(:integer) + end + end + + def now + case ActiveRecord::Base.connection.adapter_name.downcase + when /sqlserver/i + 'GETDATE()' + when /sqlite/i + "datetime('now')" + else + 'NOW()' + end + end + + def get_cf_query(id, type, table) + aggr_func = Redmine::Database.mysql? || Redmine::Database.sqlite? ? 'GROUP_CONCAT(value)' : "STRING_AGG(value, ',')" + ",(SELECT #{aggr_func} FROM custom_values WHERE custom_field_id = #{id} AND customized_type = '#{type}' AND + customized_id = #{table}.id GROUP BY custom_field_id) AS cf_#{id}" + end + + def dmsf_projects_scope + return nil unless sub_projects + + cf_columns = +'' + DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id| + cf_columns << ",NULL AS cf_#{id}" + end + scope = Project.select(%{ + projects.id AS id, + projects.id AS project_id, + CAST(NULL AS #{integer_type}) AS revision_id, + projects.name AS title, + projects.identifier AS filename, + CAST(NULL AS #{integer_type}) AS size, + projects.updated_on AS updated, + CAST(NULL AS #{integer_type}) AS major_version, + CAST(NULL AS #{integer_type}) AS minor_version, + CAST(NULL AS #{integer_type}) AS patch_version, + CAST(NULL AS #{integer_type}) AS workflow, + CAST(NULL AS #{integer_type}) AS workflow_id, + '' AS firstname, + '' AS lastname, + CAST(NULL AS #{integer_type}) AS author, + 'project' AS type, + CAST(0 AS #{integer_type}) AS deleted, + '' AS customized_type, + 0 AS customized_id, + projects.description AS description, + '' AS comment, + 0 AS locked, + 0 AS "system", + 0 AS sort#{cf_columns}}).visible + if dmsf_folder_id || deleted + scope.none + else + scope = scope.non_templates if scope.respond_to?(:non_templates) + if project.nil? && filters_on? + scope + else + scope.where projects: { parent_id: project&.id } + end + end + end + + def dmsf_folders_scope + cf_columns = +'' + DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id| + cf_columns << get_cf_query(id, 'DmsfFolder', 'dmsf_folders') + end + scope = DmsfFolder.select(%{ + dmsf_folders.id AS id, + dmsf_folders.project_id AS project_id, + CAST(NULL AS #{integer_type}) AS revision_id, + dmsf_folders.title AS title, + NULL AS filename, + CAST(NULL AS #{integer_type}) AS size, + dmsf_folders.updated_at AS updated, + CAST(NULL AS #{integer_type}) AS major_version, + CAST(NULL AS #{integer_type}) AS minor_version, + CAST(NULL AS #{integer_type}) AS patch_version, + CAST(NULL AS #{integer_type}) AS workflow, + CAST(NULL AS #{integer_type}) AS workflow_id, + users.firstname AS firstname, + users.lastname AS lastname, + users.id AS author, + 'folder' AS type, + dmsf_folders.deleted AS deleted, + 'DmsfFolder' AS customized_type, + dmsf_folders.id AS customized_id, + dmsf_folders.description AS description, + '' AS comment, + (CASE WHEN dmsf_locks.id IS NULL THEN 0 ELSE 1 END) AS locked, + (CASE WHEN (dmsf_folders.system = #{ActiveRecord::Base.connection.quoted_true}) THEN 1 ELSE 0 END) AS "system", + 1 AS sort#{cf_columns}}) + .joins('LEFT JOIN users ON dmsf_folders.user_id = users.id') + .joins("LEFT JOIN dmsf_locks ON dmsf_folders.id = dmsf_locks.entity_id AND + dmsf_locks.entity_type = 1 AND (dmsf_locks.expires_at IS NULL + OR dmsf_locks.expires_at > #{now})") + scope = deleted ? scope.deleted : scope.visible + if dmsf_folder_id + scope.where dmsf_folders: { dmsf_folder_id: dmsf_folder_id } + elsif project.nil? && filters_on? + scope + elsif statement.present? || deleted + scope.where dmsf_folders: { project_id: project&.id } + else + scope.where dmsf_folders: { project_id: project&.id, dmsf_folder_id: nil } + end + end + + def dmsf_folder_links_scope + cf_columns = +'' + DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id| + cf_columns << get_cf_query(id, 'DmsfFolder', 'dmsf_folders') + end + scope = DmsfLink.select(%{ + dmsf_links.id AS id, + dmsf_links.target_project_id AS project_id, + dmsf_links.target_id AS revision_id, + dmsf_links.name AS title, + dmsf_folders.title AS filename, + CAST(NULL AS #{integer_type}) AS size, + COALESCE(dmsf_folders.updated_at, dmsf_links.updated_at) AS updated, + CAST(NULL AS #{integer_type}) AS major_version, + CAST(NULL AS #{integer_type}) AS minor_version, + CAST(NULL AS #{integer_type}) AS patch_version, + CAST(NULL AS #{integer_type}) AS workflow, + CAST(NULL AS #{integer_type}) AS workflow_id, + users.firstname AS firstname, + users.lastname AS lastname, + users.id AS author, + 'folder-link' AS type, + dmsf_links.deleted AS deleted, + 'DmsfFolder' AS customized_type, + dmsf_folders.id AS customized_id, + dmsf_folders.description AS description, + '' AS comment, + (CASE WHEN dmsf_locks.id IS NULL THEN 0 ELSE 1 END) AS locked, + 0 AS "system", + 1 AS sort#{cf_columns}}) + .joins('LEFT JOIN dmsf_folders ON dmsf_links.target_id = dmsf_folders.id') + .joins('LEFT JOIN users ON users.id = COALESCE(dmsf_folders.user_id, dmsf_links.user_id)') + .joins("LEFT JOIN dmsf_locks ON dmsf_folders.id = dmsf_locks.entity_id AND + dmsf_locks.entity_type = 1 AND (dmsf_locks.expires_at IS NULL OR + dmsf_locks.expires_at > #{now})") + scope = deleted ? scope.deleted : scope.visible + if dmsf_folder_id + scope.where dmsf_links: { target_type: 'DmsfFolder', dmsf_folder_id: dmsf_folder_id } + elsif project.nil? && filters_on? + scope + elsif statement.present? || deleted + scope.where dmsf_links: { target_type: 'DmsfFolder', project_id: project&.id } + else + scope.where dmsf_links: { target_type: 'DmsfFolder', project_id: project&.id, dmsf_folder_id: nil } + end + end + + def dmsf_files_scope + cf_columns = +'' + DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id| + cf_columns << get_cf_query(id, 'DmsfFileRevision', 'dmsf_file_revisions') + end + scope = DmsfFile.select(%{ + dmsf_files.id AS id, + dmsf_files.project_id AS project_id, + dmsf_file_revisions.id AS revision_id, + dmsf_file_revisions.title AS title, + dmsf_file_revisions.name AS filename, + dmsf_file_revisions.size AS size, + dmsf_file_revisions.updated_at AS updated, + dmsf_file_revisions.major_version AS major_version, + dmsf_file_revisions.minor_version AS minor_version, + dmsf_file_revisions.patch_version AS patch_version, + dmsf_file_revisions.workflow AS workflow, + dmsf_file_revisions.dmsf_workflow_id AS workflow_id, + users.firstname AS firstname, + users.lastname AS lastname, + users.id AS author, + 'file' AS type, + dmsf_files.deleted AS deleted, + 'DmsfFileRevision' AS customized_type, + dmsf_file_revisions.id AS customized_id, + dmsf_file_revisions.description AS description, + dmsf_file_revisions.comment AS comment, + (CASE WHEN dmsf_locks.id IS NULL THEN 0 ELSE 1 END) AS locked, + 0 AS "system", + 2 AS sort#{cf_columns}}) + .joins(:dmsf_file_revisions) + .joins('LEFT JOIN users ON dmsf_file_revisions.user_id = users.id ') + .joins("LEFT JOIN dmsf_locks ON dmsf_files.id = dmsf_locks.entity_id AND dmsf_locks.entity_type = 0 + AND (dmsf_locks.expires_at IS NULL OR dmsf_locks.expires_at > #{now})") + .where(sub_query) + scope = deleted ? scope.deleted : scope.visible + if dmsf_folder_id + scope.where dmsf_files: { dmsf_folder_id: dmsf_folder_id } + elsif project.nil? && filters_on? + scope + elsif statement.present? || deleted + scope.where dmsf_files: { project_id: project&.id } + else + scope.where(dmsf_files: { project_id: project&.id, dmsf_folder_id: nil }) + end + end + + def dmsf_file_links_scope + cf_columns = +'' + DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id| + cf_columns << get_cf_query(id, 'DmsfFileRevision', 'dmsf_file_revisions') + end + scope = DmsfLink.select(%{ + dmsf_links.id AS id, + dmsf_files.project_id AS project_id, + dmsf_files.id AS revision_id, + dmsf_links.name AS title, + dmsf_file_revisions.name AS filename, + dmsf_file_revisions.size AS size, + dmsf_file_revisions.updated_at AS updated, + dmsf_file_revisions.major_version AS major_version, + dmsf_file_revisions.minor_version AS minor_version, + dmsf_file_revisions.patch_version AS patch_version, + dmsf_file_revisions.workflow AS workflow, + dmsf_file_revisions.dmsf_workflow_id AS workflow_id, + users.firstname AS firstname, + users.lastname AS lastname, + users.id AS author, + 'file-link' AS type, + dmsf_links.deleted AS deleted, + 'DmsfFileRevision' AS customized_type, + dmsf_file_revisions.id AS customized_id, + dmsf_file_revisions.description AS description, + dmsf_file_revisions.comment AS comment, + (CASE WHEN dmsf_locks.id IS NULL THEN 0 ELSE 1 END) AS locked, + 0 AS "system", + 2 AS sort#{cf_columns}}) + .joins('JOIN dmsf_files ON dmsf_files.id = dmsf_links.target_id') + .joins('JOIN dmsf_file_revisions ON dmsf_file_revisions.dmsf_file_id = dmsf_files.id') + .joins('LEFT JOIN users ON dmsf_file_revisions.user_id = users.id ') + .joins("LEFT JOIN dmsf_locks ON dmsf_files.id = dmsf_locks.entity_id AND dmsf_locks.entity_type = 0 + AND (dmsf_locks.expires_at IS NULL OR dmsf_locks.expires_at > #{now})") + .where(sub_query) + scope = deleted ? scope.deleted : scope.visible + if dmsf_folder_id + scope.where dmsf_links: { target_type: 'DmsfFile', dmsf_folder_id: dmsf_folder_id } + elsif project.nil? && filters_on? + scope + elsif statement.present? || deleted + scope.where dmsf_links: { target_type: 'DmsfFile', project_id: project&.id } + else + scope.where dmsf_links: { target_type: 'DmsfFile', project_id: project&.id, dmsf_folder_id: nil } + end + end + + def dmsf_url_links_scope + cf_columns = +'' + DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id| + cf_columns << ",NULL AS cf_#{id}" + end + scope = DmsfLink.select(%{ + dmsf_links.id AS id, + dmsf_links.project_id AS project_id, + CAST(NULL AS #{integer_type}) AS revision_id, + dmsf_links.name AS title, + dmsf_links.external_url AS filename, + CAST(NULL AS #{integer_type}) AS size, + dmsf_links.updated_at AS updated, + CAST(NULL AS #{integer_type}) AS major_version, + CAST(NULL AS #{integer_type}) AS minor_version, + CAST(NULL AS #{integer_type}) AS patch_version, + CAST(NULL AS #{integer_type}) AS workflow, + CAST(NULL AS #{integer_type}) AS workflow_id, + users.firstname AS firstname, + users.lastname AS lastname, + users.id AS author, + 'url-link' AS type, + dmsf_links.deleted AS deleted, + '' AS customized_type, + 0 AS customized_id, + '' AS description, + '' AS comment, + 0 AS locked, + 0 AS "system", + 2 AS sort#{cf_columns}}) + .joins('LEFT JOIN users ON dmsf_links.user_id = users.id ') + scope = deleted ? scope.deleted : scope.visible + if dmsf_folder_id + scope.where dmsf_links: { target_type: 'DmsfUrl', dmsf_folder_id: dmsf_folder_id } + elsif project.nil? && filters_on? + scope + elsif statement.present? || deleted + scope.where dmsf_links: { target_type: 'DmsfUrl', project_id: project&.id } + else + scope.where dmsf_links: { target_type: 'DmsfUrl', project_id: project&.id, dmsf_folder_id: nil } + end + end +end diff --git a/app/models/dmsf_query_modified_column.rb b/app/models/dmsf_query_modified_column.rb new file mode 100644 index 00000000..7294256e --- /dev/null +++ b/app/models/dmsf_query_modified_column.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require 'query' + +# Modify column +class DmsfQueryModifiedColumn < QueryColumn + def value_object(object) + object.updated + end +end diff --git a/app/models/dmsf_query_title_column.rb b/app/models/dmsf_query_title_column.rb new file mode 100644 index 00000000..b1c71e6c --- /dev/null +++ b/app/models/dmsf_query_title_column.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require 'query' + +# Title column +class DmsfQueryTitleColumn < QueryColumn + def css_classes + 'dmsf-title' + end +end diff --git a/app/models/dmsf_query_version_column.rb b/app/models/dmsf_query_version_column.rb new file mode 100644 index 00000000..f5215ce1 --- /dev/null +++ b/app/models/dmsf_query_version_column.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require 'query' + +# Version column +class DmsfQueryVersionColumn < QueryColumn + def value_object(object) + DmsfFileRevision.version object.major_version, object.minor_version, object.patch_version + end +end diff --git a/app/models/dmsf_upload.rb b/app/models/dmsf_upload.rb new file mode 100644 index 00000000..f1818340 --- /dev/null +++ b/app/models/dmsf_upload.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +# 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 + attr_reader :size + + def disk_file + Rails.root.join 'tmp', disk_filename + end + + def self.create_from_uploaded_attachment(project, folder, uploaded_file) + a = Attachment.find_by_token(uploaded_file[:token]) if uploaded_file[:token].present? + if a + uploaded = { + disk_filename: DmsfHelper.temp_filename(a.filename), + content_type: a.content_type, + original_filename: a.filename, + comment: uploaded_file[:description], + tempfile_path: a.diskfile, + token: uploaded_file[:token], + digest: a.digest + } + DmsfUpload.new project, folder, uploaded + else + Rails.logger.error "An attachment not found by its token: #{uploaded_file[:token]}" + nil + end + end + + def initialize(project, folder = nil, uploaded = nil) + unless uploaded + @name = '' + @disk_filename = '' + @mime_type = '' + @size = 0 + @tempfile_path = '' + @token = '' + @digest = '' + if RedmineDmsf.empty_minor_version_by_default? + @major_version = 1 + @minor_version = nil + else + @major_version = 0 + @minor_version = 0 + end + @patch_version = nil + @workflow = nil + revision = DmsfFileRevision.new + @custom_values = revision.custom_field_values + return + end + + @name = uploaded[:original_filename] + + file = DmsfFile.find_file_by_name(project, folder, @name) + unless file + link = DmsfLink.find_link_by_file_name(project, folder, @name) + file = link.target_file if link + end + + @disk_filename = uploaded[:disk_filename] + @mime_type = uploaded[:content_type] + @size = File.size?(uploaded[:tempfile_path]) + unless @size + @size = 0 + Rails.logger.error "Cannot find #{uploaded[:tempfile_path]}" + 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) + @description = uploaded[:comment] + if RedmineDmsf.empty_minor_version_by_default? + @major_version = 1 + @minor_version = nil + else + @major_version = 0 + @minor_version = 0 + end + @patch_version = nil + @workflow = nil + file = DmsfFile.new + file.project_id = project.id + revision = DmsfFileRevision.new + revision.dmsf_file = file + @custom_values = revision.custom_field_values + else + last_revision = file.last_revision + @title = last_revision.title + if last_revision.description.present? + @description = last_revision.description + @comment = uploaded[:comment] if uploaded[:comment].present? + elsif uploaded[:comment].present? + @comment = uploaded[:comment] + end + @major_version = last_revision.major_version + @minor_version = last_revision.minor_version + @patch_version = last_revision.patch_version + @workflow = last_revision.workflow + @custom_values = Array.new(file.last_revision.custom_values) + + # Add default value for CFs not existing + present_custom_fields = file.last_revision.custom_values.collect(&:custom_field).uniq + file.last_revision.available_custom_fields.each do |cf| + if cf.default_value && present_custom_fields.exclude?(cf) + @custom_values << CustomValue.new({ custom_field: cf, value: cf.default_value }) + end + end + end + @locked = file&.locked_for_user? + end +end diff --git a/app/models/dmsf_workflow.rb b/app/models/dmsf_workflow.rb new file mode 100644 index 00000000..95e70238 --- /dev/null +++ b/app/models/dmsf_workflow.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Workflow +class DmsfWorkflow < ApplicationRecord + has_many :dmsf_workflow_steps, -> { order(step: :asc) }, dependent: :destroy, inverse_of: :dmsf_workflow + belongs_to :author, class_name: 'User' + + scope :sorted, -> { order name: :asc } + scope :global, -> { where project_id: nil } + scope :active, -> { where status: STATUS_ACTIVE } + scope :status, ->(arg) { where arg.blank? ? nil : { status: arg.to_i } } + + validates :name, presence: true, length: { maximum: 255 }, dmsf_workflow_name: true + + STATE_ASSIGNED = 3 + STATE_WAITING_FOR_APPROVAL = 1 + STATE_APPROVED = 2 + STATE_REJECTED = 4 + STATE_OBSOLETE = 5 + + STATUS_LOCKED = 0 + STATUS_ACTIVE = 1 + + def self.workflow_str(workflow) + case workflow + when STATE_WAITING_FOR_APPROVAL + l(:title_waiting_for_approval) + when STATE_APPROVED + l(:title_approved) + when STATE_ASSIGNED + l(:title_assigned) + when STATE_REJECTED + l(:title_rejected) + when DmsfWorkflow::STATE_OBSOLETE + l(:title_obsolete) + end + end + + def self.workflow_info(workflow, workflow_id, revision_id) + text = '' + names = '' + if workflow.to_i == STATE_WAITING_FOR_APPROVAL + dmsf_workflow = DmsfWorkflow.find_by(id: workflow_id) + if dmsf_workflow + assignments = dmsf_workflow.next_assignments(revision_id) + if assignments.any? + user_ids = assignments.map(&:user_id) + names = User.where(id: user_ids).all.map(&:name).join(',') + workflow_step_id = assignments.first[:dmsf_workflow_step_id] + if workflow_step_id + step = DmsfWorkflowStep.find_by(id: workflow_step_id) + text = step.name if step&.name.present? + end + end + end + end + text = DmsfWorkflow.workflow_str(workflow.to_i) if text.blank? + [text, names] + end + + def participiants + users = [] + dmsf_workflow_steps.each do |step| + users << step.user unless users.include? step.user + end + users + end + + def self.workflows(project) + where(project_id: project) + end + + def project + Project.find_by(id: project_id) if project_id + end + + def to_s + name + end + + def reorder_steps(step, move_to) + DmsfWorkflow.transaction do + dmsf_workflow_steps.each do |ws| + if ws.step == step + ws.update_attribute :step, move_to + elsif ws.step >= move_to && ws.step < step + # Move up + ws.update_attribute :step, ws.step + 1 + elsif ws.step <= move_to && ws.step > step + # Move down + ws.update_attribute :step, ws.step - 1 + end + end + end + end + + def delegates(query, dmsf_workflow_step_assignment_id, dmsf_file_revision_id) + sql = if dmsf_workflow_step_assignment_id && dmsf_file_revision_id + [ + %(id NOT IN (SELECT a.user_id FROM dmsf_workflow_step_assignments a WHERE id = ?) + AND id IN (SELECT m.user_id FROM members m JOIN dmsf_files f ON f.project_id = m.project_id + JOIN dmsf_file_revisions r ON r.dmsf_file_id = f.id WHERE r.id = ?)), + dmsf_workflow_step_assignment_id, dmsf_file_revision_id + ] + elsif project + ['id IN (SELECT user_id FROM members WHERE project_id = ?)', project.id] + else + '1=1' + end + + if query.present? + User.active.sorted.where(sql).like query + else + User.active.sorted.where sql + end + end + + def next_assignments(dmsf_file_revision_id) + results = [] + nsteps = dmsf_workflow_steps.collect(&:step).uniq + nsteps.each do |i| + step_is_finished = false + steps = dmsf_workflow_steps.filter_map { |s| s.step == i ? s : nil } + steps.each do |step| + step.dmsf_workflow_step_assignments.where(dmsf_file_revision_id: dmsf_file_revision_id) + .find_each do |assignment| + assignment.dmsf_workflow_step_actions.find_each do |action| + case action.action + when DmsfWorkflowStepAction::ACTION_APPROVE + step_is_finished = true + # Try to find another unfinished AND step + exists = false + stps = dmsf_workflow_steps + .filter_map { |s| s.step == i && s.operator == DmsfWorkflowStep::OPERATOR_AND ? s : nil } + stps.each do |s| + s.dmsf_workflow_step_assignments.where(dmsf_file_revision_id: dmsf_file_revision_id).find_each do |a| + exists = a.add?(dmsf_file_revision_id) + break if exists + end + break if exists + end + step_is_finished = false if exists + break + when DmsfWorkflowStepAction::ACTION_REJECT + return [] + end + end + break if step_is_finished + end + break if step_is_finished + end + next if step_is_finished + + steps.each do |step| + step.dmsf_workflow_step_assignments.where(dmsf_file_revision_id: dmsf_file_revision_id) + .find_each do |assignment| + results << assignment if assignment.add?(dmsf_file_revision_id) + end + end + return results + end + results + end + + def assign(dmsf_file_revision_id) + dmsf_workflow_steps.each do |ws| + ws.assign(dmsf_file_revision_id) + end + end + + def try_finish?(revision, action, user_id) + case action.action + when DmsfWorkflowStepAction::ACTION_APPROVE + assignments = next_assignments(revision.id) + return false unless assignments.empty? + + revision.workflow = DmsfWorkflow::STATE_APPROVED + revision.save! + return true + when DmsfWorkflowStepAction::ACTION_REJECT + revision.workflow = DmsfWorkflow::STATE_REJECTED + revision.save! + return true + when DmsfWorkflowStepAction::ACTION_DELEGATE + dmsf_workflow_steps.each do |step| + step.dmsf_workflow_step_assignments.each do |assignment| + next unless assignment.id == action.dmsf_workflow_step_assignment_id + + assignment.user_id = user_id + assignment.save! + return false + end + end + end + false + end + + def copy_to(project, name = nil) + new_wf = dup + new_wf.name = name if name + new_wf.project_id = project&.id + new_wf.author = User.current + if new_wf.save + dmsf_workflow_steps.each do |step| + step.copy_to(new_wf) + end + end + new_wf + end + + def locked? + status == STATUS_LOCKED + end + + def active? + status == STATUS_ACTIVE + end + + def notify_users(project, revision, controller) + assignments = next_assignments(revision.id) + recipients = assignments.collect(&:user).uniq + recipients &= DmsfMailer.get_notify_users(project, revision.dmsf_file, force_notification: true) + DmsfMailer.deliver_workflow_notification( + recipients, + self, + revision, + :text_email_subject_started, + :text_email_started, + :text_email_to_proceed, + nil, + assignments.first&.dmsf_workflow_step + ) + return unless RedmineDmsf.dmsf_display_notified_recipients? && controller && recipients.present? + + max_recipients = RedmineDmsf.dmsf_max_notification_receivers_info + to = recipients.collect(&:name).first(max_recipients).join(', ') + return if to.blank? + + to << (recipients.count > max_recipients.to_i ? ',...' : '.') + controller.flash[:warning] = l(:warning_email_notifications, to: to) if controller + end +end diff --git a/app/models/dmsf_workflow_step.rb b/app/models/dmsf_workflow_step.rb new file mode 100644 index 00000000..23e71110 --- /dev/null +++ b/app/models/dmsf_workflow_step.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Workflow step +class DmsfWorkflowStep < ApplicationRecord + belongs_to :dmsf_workflow + belongs_to :user + + has_many :dmsf_workflow_step_assignments, dependent: :destroy + + validates :step, presence: true + validates :operator, presence: true + validates :user_id, uniqueness: { scope: %i[dmsf_workflow_id step], case_sensitive: true } + validates :name, length: { maximum: 30 } + + OPERATOR_OR = 0 + OPERATOR_AND = 1 + + def soperator + DmsfWorkflowStep.soperator(operator) + end + + def self.soperator(operator) + operator == 1 ? l(:dmsf_and) : l(:dmsf_or) + end + + def assign(dmsf_file_revision_id) + step_assignment = DmsfWorkflowStepAssignment.new + step_assignment.dmsf_workflow_step_id = id + step_assignment.user_id = user_id + step_assignment.dmsf_file_revision_id = dmsf_file_revision_id + step_assignment.save! + end + + def finished?(dmsf_file_revision_id) + dmsf_workflow_step_assignments.each do |assignment| + next if assignment.dmsf_file_revision_id == dmsf_file_revision_id + + return false if assignment.dmsf_workflow_step_actions.empty? + + assignment.dmsf_workflow_step_actions.each do |act| + return false unless act.finished? + end + end + end + + def copy_to(workflow) + new_step = dup + new_step.dmsf_workflow_id = workflow.id + new_step.save! + new_step + end +end diff --git a/app/models/dmsf_workflow_step_action.rb b/app/models/dmsf_workflow_step_action.rb new file mode 100644 index 00000000..d091945c --- /dev/null +++ b/app/models/dmsf_workflow_step_action.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Workflow step action +class DmsfWorkflowStepAction < ApplicationRecord + belongs_to :dmsf_workflow_step_assignment + belongs_to :author, class_name: 'User' + + validates :action, presence: true + validates :note, presence: true, unless: -> { action == DmsfWorkflowStepAction::ACTION_APPROVE } + validates :dmsf_workflow_step_assignment_id, + unless: -> { action == DmsfWorkflowStepAction::ACTION_DELEGATE }, + uniqueness: { scope: [:action], case_sensitive: true } + + ACTION_APPROVE = 1 + ACTION_REJECT = 2 + ACTION_DELEGATE = 3 + ACTION_ASSIGN = 4 + ACTION_START = 5 + + def initialize(*args) + super + self.author_id = User.current.id if User.current + end + + def self.finished?(action) + [DmsfWorkflowStepAction::ACTION_APPROVE, DmsfWorkflowStepAction::ACTION_REJECT].include? action + end + + def finished? + DmsfWorkflowStepAction.finished? action + end + + def self.action_str(action) + return unless action + + case action.to_i + when ACTION_APPROVE + l(:title_approval) + when ACTION_REJECT + l(:title_rejection) + when ACTION_DELEGATE + l(:title_delegation) + when ACTION_ASSIGN + l(:title_assignment) + when ACTION_START + l(:title_start) + end + end + + def self.action_type_str(action) + return unless action + + case action.to_i + when ACTION_APPROVE + 'approval' + when ACTION_REJECT + 'rejection' + when ACTION_DELEGATE + 'delegation' + when ACTION_ASSIGN + 'assignment' + when ACTION_START + 'start' + end + end + + def self.workflow_str(action) + return unless action + + case action.to_i + when ACTION_REJECT + l(:title_rejected) + when ACTION_ASSIGN + l(:title_assigned) + when ACTION_START, ACTION_DELEGATE, ACTION_APPROVE + l(:title_waiting_for_approval) + else + l(:title_none) + end + end +end diff --git a/app/models/dmsf_workflow_step_assignment.rb b/app/models/dmsf_workflow_step_assignment.rb new file mode 100644 index 00000000..4add64f1 --- /dev/null +++ b/app/models/dmsf_workflow_step_assignment.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Workflow step assignment +class DmsfWorkflowStepAssignment < ApplicationRecord + belongs_to :dmsf_workflow_step + belongs_to :user + belongs_to :dmsf_file_revision + + has_many :dmsf_workflow_step_actions, dependent: :destroy + + validates :dmsf_workflow_step_id, uniqueness: { scope: [:dmsf_file_revision_id], case_sensitive: true } + + def add?(dmsf_file_revision_id) + if dmsf_file_revision_id == self.dmsf_file_revision_id + add = true + dmsf_workflow_step_actions.each do |action| + if action.finished? + add = false + break + end + end + return add + end + false + end +end diff --git a/app/validators/dmsf_file_extension_validator.rb b/app/validators/dmsf_file_extension_validator.rb new file mode 100644 index 00000000..7f8cc851 --- /dev/null +++ b/app/validators/dmsf_file_extension_validator.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# File extension validator according to the Redmine whitelist and blacklist for file upload. +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)) + end +end diff --git a/app/validators/dmsf_file_name_validator.rb b/app/validators/dmsf_file_name_validator.rb new file mode 100644 index 00000000..0e826998 --- /dev/null +++ b/app/validators/dmsf_file_name_validator.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# File name validator +class DmsfFileNameValidator < ActiveModel::EachValidator + ALL_INVALID_CHARACTERS = /\A[^#{DmsfFolder::INVALID_CHARACTERS}]*\z/ + + def validate_each(record, attribute, value) + record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value) + end +end diff --git a/app/validators/dmsf_folder_parent_validator.rb b/app/validators/dmsf_folder_parent_validator.rb new file mode 100644 index 00000000..93ba6dfb --- /dev/null +++ b/app/validators/dmsf_folder_parent_validator.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Folder parent validator +class DmsfFolderParentValidator < ActiveModel::EachValidator + # Don't allow save folders with a parent pointing to the same folder + def validate_each(record, attribute, value) + folder = value + while folder + if folder == record + record.errors.add attribute, :invalid + return + end + folder = folder.dmsf_folder + end + end +end diff --git a/app/validators/dmsf_max_file_size_validator.rb b/app/validators/dmsf_max_file_size_validator.rb new file mode 100644 index 00000000..d9c9c470 --- /dev/null +++ b/app/validators/dmsf_max_file_size_validator.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Max file size validator +class DmsfMaxFileSizeValidator < ActiveModel::EachValidator + include Redmine::I18n + + def validate_each(record, attribute, value) + return unless value && (value > Setting.attachment_max_size.to_i.kilobytes) + + record.errors.add attribute, + l(:error_attachment_too_big, max_size: ActiveSupport::NumberHelper.number_to_human_size( + Setting.attachment_max_size.to_i.kilobytes + )) + end +end diff --git a/app/validators/dmsf_url_validator.rb b/app/validators/dmsf_url_validator.rb new file mode 100644 index 00000000..dce6cf9e --- /dev/null +++ b/app/validators/dmsf_url_validator.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# URL validator +class DmsfUrlValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return unless record.target_type == 'DmsfUrl' + + begin + if value.present? + # https://www.google.com/search?q=寿司 + URI.parse value.split('?').first + else + record.errors.add attribute, :invalid + end + rescue URI::InvalidURIError => e + record.errors.add attribute, :invalid + Rails.logger.error e.message + end + end +end diff --git a/app/validators/dmsf_workflow_name_validator.rb b/app/validators/dmsf_workflow_name_validator.rb new file mode 100644 index 00000000..3737aa2f --- /dev/null +++ b/app/validators/dmsf_workflow_name_validator.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Workflow name validator +class DmsfWorkflowNameValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if record.project_id + if record.id + if DmsfWorkflow.exists?(['(project_id IS NULL OR (project_id = ? AND id != ?)) AND name = ?', + record.project_id, + record.id, value]) + record.errors.add attribute, :taken + end + elsif DmsfWorkflow.exists?(['(project_id IS NULL OR project_id = ?) AND name = ?', record.project_id, value]) + record.errors.add attribute, :taken + end + elsif record.id + record.errors.add attribute, :taken if DmsfWorkflow.exists?(['name = ? AND id != ?', value, record.id]) + elsif DmsfWorkflow.exists?(name: value) + record.errors.add attribute, :taken + end + end +end diff --git a/app/views/dmsf/_add_email.html.erb b/app/views/dmsf/_add_email.html.erb new file mode 100644 index 00000000..9151550d --- /dev/null +++ b/app/views/dmsf/_add_email.html.erb @@ -0,0 +1,34 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

<%= l(:label_email_address_add) %>

+ +<%= form_tag(append_email_dmsf_path(id: @project), remote: true, method: :post, id: 'new-user-form') do %> + <%= hidden_field_tag :project_id, @project.id %> +

<%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %>

+ <%= javascript_tag "observeSearchfield('user_search', 'users_for_users', '#{ escape_javascript url_for( + controller: 'dmsf', action: 'autocomplete_for_user') }')" %> +
+ <%= render_principals_for_new_email @principals %> +
+

+ <%= submit_tag l(:button_add), name: nil, onclick: 'hideModal(this);' %> + <%= submit_tag l(:button_cancel), name: nil, onclick: 'hideModal(this);', type: 'button' %> +

+<% end %> diff --git a/app/views/dmsf/_custom_fields.html.erb b/app/views/dmsf/_custom_fields.html.erb new file mode 100644 index 00000000..5ba49b4e --- /dev/null +++ b/app/views/dmsf/_custom_fields.html.erb @@ -0,0 +1,33 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% if object %> +
+ <% object.custom_field_values.each do |custom_value| %> + <% unless custom_value.value.blank? %> +
+ <%= content_tag :div, h(custom_value.custom_field.name), class: 'label' %> +
+ <%= show_value custom_value %> +
+
+ <% end %> + <% end %> +
+<% end %> diff --git a/app/views/dmsf/_description.html.erb b/app/views/dmsf/_description.html.erb new file mode 100644 index 00000000..a1829256 --- /dev/null +++ b/app/views/dmsf/_description.html.erb @@ -0,0 +1,35 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% if @project %> +
+
+ <%= textilizable @folder ? @folder.description : @project.dmsf_description %> +
+ <% if @folder && @folder.custom_field_values.any? { |o| o.value.present? } %> +
    + <% render_custom_field_values(@folder) do |custom_field, formatted| %> +
  • + <%= custom_field.name %>: <%= formatted %> +
  • + <% end %> +
+ <% end %> +
+<% end %> diff --git a/app/views/dmsf/_digest.html.erb b/app/views/dmsf/_digest.html.erb new file mode 100644 index 00000000..1e5e1c8c --- /dev/null +++ b/app/views/dmsf/_digest.html.erb @@ -0,0 +1,29 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

+ <%= l(:text_dmsf_webdav_digest_reset) %> +

+<%= form_tag(dmsf_reset_digest_path) do %> + + <%= password_field_tag 'password', nil, autofocus: true %> + +<% end %> diff --git a/app/views/dmsf/_main.html.erb b/app/views/dmsf/_main.html.erb new file mode 100644 index 00000000..4f1fb387 --- /dev/null +++ b/app/views/dmsf/_main.html.erb @@ -0,0 +1,96 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% html_title l(:dmsf) %> +<% if @project %> +
+ <% 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' } %> + <% 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' } %> + <% end %> + <% end %> + <%= actions_dropdown do %> + <%= render partial: 'dmsf_context_menus/main', locals: { + folder_manipulation_allowed: @folder_manipulation_allowed, + system_folder: @system_folder, + folder: @folder, + project: @project, + locked: @locked, + file_manipulation_allowed: @file_manipulation_allowed, + trash_enabled: @trash_enabled, + notifications: @notifications} %> + <% end %> +
+<% end %> + +<%= render partial: 'path', locals: { folder: @folder, filename: nil, title: nil } %> + +<%= render partial: 'description' %> + +<%= form_tag(@project ? dmsf_folder_path(id: @project, folder_id: @folder) : dmsf_index_path, + method: :get, id: 'query_form', class: 'dmsf-query-form') do %> + <%= hidden_field_tag('folder_id', @folder.id) if @folder %> +
+ <%= render partial: 'queries/query_form' %> +
+<% end %> +<% if @query.valid? %> + <% if @dmsf_count == 0 %> +

<%= l(:label_no_data) %>

+ <% else %> + <%= render partial: 'query_list', locals: { query: @query, dmsf_pages: @dmsf_pages, folder: @folder } %> + <%= pagination_links_full @dmsf_pages, @dmsf_count %> + <% end %> +<% end %> + +<%= context_menu %> + +<% if !@folder&.system && (@project || RedmineDmsf.dmsf_webdav?) %> + <% other_formats_links do |f| %> + <% if @project %> + <%= f.link_to 'CSV', url: { action: :show, id: @project, folder_id: @folder, encoding: Encoding::UTF_8 } %> + <% end %> + <% if RedmineDmsf.dmsf_webdav? %> + + <%= link_to 'WebDAV', webdav_url(@project, @folder), class: 'webdav' %> + + <% end %> + <% end %> +<% end %> + +<% content_for :sidebar do %> + <%= render partial: 'dmsf/sidebar' %> + + <% project_or_folder = @folder? @folder : @project %> + <% if project_or_folder&.watchers.present? && User.current.allowed_to?(:view_dmsf_folder_watchers, @project) %> +
+ <%= render partial: 'watchers/watchers', locals: { watched: @folder ? @folder : @project } %> +
+ <% end %> +<% end %> + +<% javascript_tag do %> + "$('#ajax-indicator').hide();" +<% end %> \ No newline at end of file diff --git a/app/views/dmsf/_path.html.erb b/app/views/dmsf/_path.html.erb new file mode 100644 index 00000000..bf30da80 --- /dev/null +++ b/app/views/dmsf/_path.html.erb @@ -0,0 +1,49 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

+ <% if folder %> + <%= link_to l(:link_documents), dmsf_folder_path(id: @project) %> + <% folder.dmsf_path.each do |path_element| %> + / + <% if filename.blank? && (path_element == folder.dmsf_path.last) %> + <%= h(path_element.title) %> + <% else %> + <%= link_to h(path_element.title), dmsf_folder_path(id: @project, folder_id: path_element) %> + <% end %> + <% end %> + <% if folder.locked? && !(filename || title) %> + <%= content_tag('span', sprite_icon('unlock', nil, icon_only: true, size: '12'), + title: l(:title_locked_by_user, user: folder.locked_by)) %> + <% end %> + <% else %> + <% if @project %> + <%= link_to l(:link_documents), dmsf_folder_path(id: @project) %> + <% else %> + <%= l(:link_documents) %> + <% end %> + <% end %> + <% if filename %> + / + <%= h(filename) %> + <% end %> + <% if title %> + » <%= title %> + <% end %> +

diff --git a/app/views/dmsf/_query_list.html.erb b/app/views/dmsf/_query_list.html.erb new file mode 100644 index 00000000..0b5da808 --- /dev/null +++ b/app/views/dmsf/_query_list.html.erb @@ -0,0 +1,44 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% query_options = nil unless defined?(query_options) %> +<% query_options ||= {} %> + +<%= form_tag({}, data: { cm_url: query.deleted ? dmsf_trash_context_menu_path : dmsf_context_menu_path(folder_id: folder) }) do %> + <%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %> + <%= query_columns_hidden_tags(query) %> +
+ + + + + <% query.inline_columns.each do |column| %> + <%= column_header(query, column, query_options) %> + <% end %> + + + + + <%= render partial: 'query_rows', locals: { query: query, dmsf_pages: dmsf_pages } %> + +
+ <%= check_box_tag 'check_all', '', false, class: 'toggle-selection', title: "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> +
+
+<% end %> diff --git a/app/views/dmsf/_query_rows.erb b/app/views/dmsf/_query_rows.erb new file mode 100644 index 00000000..3ccc2ff4 --- /dev/null +++ b/app/views/dmsf/_query_rows.erb @@ -0,0 +1,110 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% options = {} %> +<% if dmsf_pages %> + <% options[:offset] = dmsf_pages.offset %> + <% options[:limit] = dmsf_pages.per_page %> +<% end %> +<% query.dmsf_nodes(options).each do |node| %> + <% case node.type %> + <% when 'project'%> + <% id = "#{node.id}pspan" %> + <% when 'folder' %> + <% id = "#{node.id}fspan" %> + <% else %> + <% id = "#{node.id}item" %> + <% end %> + <% @idnt ||= 0 %> + "> + + <%= check_box_tag('ids[]', "#{node.type}-#{node.id}", false, id: nil) unless node.system %> + + <% query.inline_columns.each do |column| %> + <% classes = column.css_classes.to_s.dup %> + <% classes << ' dmsf-gray' if node.type.match?(/link$/) %> + <% classes << ' dmsf-system' if node.system %> + <%= content_tag 'td', column_content(column, node), class: classes %> + <% end %> + + <% unless node.system %> + <%= link_to_context_menu %> + <% end %> + + +<% end %> + +<% unless query.deleted || query&.project.nil? %> + <%= javascript_tag do %> + $(function() { + $("table.dmsf tr").removeClass("ui-draggable-dragging ui-droppable-active ui-droppable-hover ui-draggable-handle"); + $(".dmsf-draggable").draggable({ + helper: function(event, ui) { + var ret = $(this).clone(); + var width = $(this)[0].offsetWidth; + var myHelper = []; + myHelper.push(''); + myHelper.push(ret.html()); + myHelper.push('
'); + helper = myHelper.join(''); + return helper; + }, + axis: "y", + revert: "invalid" + }); + $(".dmsf-droppable" ).droppable({ + drop: function(event, ui) { + let handle = $(this); + let dragObjectId = ui.draggable.find("td").find("input").val() + let dropObjectId = handle.attr('id'); + let data = {}; + handle.addClass("ui-state-highlight ajax-loading") + data['dmsf_folder'] = { drag_id: dragObjectId, drop_id: dropObjectId}; + $.ajax({ + url: '<%= dmsf_folder_url(query.project) %>', + type: 'put', + dataType: 'script', + data: data, + error: function(jqXHR, textStatus, errorThrown){ + alert(errorThrown); + ui.draggable.animate(ui.draggable.data("ui-draggable").originalPosition, "slow"); + }, + complete: function(jqXHR, textStatus, errorThrown){ + handle.removeClass("ui-state-highlight ajax-loading") + if(textStatus == 'success'){ + if(!handle.hasClass('dmsf-not-loaded')){ + var m = handle.attr("id").match(/^(\d+)span$/); + if(m){ + $('.' + m[1]).remove(); + handle.removeClass("dmsf-expanded"); + handle.addClass("dmsf-collapsed dmsf-not-loaded"); + } + ui.draggable.remove(); + } + window.location.reload(); + } + } + }); + } + }); + }); + <% end %> +<% end %> diff --git a/app/views/dmsf/_sidebar.html.erb b/app/views/dmsf/_sidebar.html.erb new file mode 100644 index 00000000..332ddbea --- /dev/null +++ b/app/views/dmsf/_sidebar.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= render_sidebar_queries DmsfQuery, @project %> diff --git a/app/views/dmsf/add_email.js.erb b/app/views/dmsf/add_email.js.erb new file mode 100644 index 00000000..1a1e2fa4 --- /dev/null +++ b/app/views/dmsf/add_email.js.erb @@ -0,0 +1,24 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 +# . +%> + +var modal = $('#ajax-modal'); + +modal.html("<%= escape_javascript(render partial: 'dmsf/add_email') %>"); +showModal('ajax-modal', '400px'); +modal.addClass('new-user'); diff --git a/app/views/dmsf/append_email.js.erb b/app/views/dmsf/append_email.js.erb new file mode 100644 index 00000000..d5418d90 --- /dev/null +++ b/app/views/dmsf/append_email.js.erb @@ -0,0 +1,30 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 +# . +%> + +let to = $('#email_to'); +let text = to.val(); +<% @principals.each do |p| %> + if(text.indexOf("<%= p.mail %>") < 0){ + if(text.length > 0){ + text += ', ' + } + text += "<%= p.mail %>" + } +<% end %> +to.val(text); diff --git a/app/views/dmsf/autocomplete_for_user.js.erb b/app/views/dmsf/autocomplete_for_user.js.erb new file mode 100644 index 00000000..c2de8c80 --- /dev/null +++ b/app/views/dmsf/autocomplete_for_user.js.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +$('#users_for_watcher').html('<%= escape_javascript(render_principals_for_new_email(@principals)) %>'); diff --git a/app/views/dmsf/copymove.html.erb b/app/views/dmsf/copymove.html.erb new file mode 100644 index 00000000..35da3fe1 --- /dev/null +++ b/app/views/dmsf/copymove.html.erb @@ -0,0 +1,72 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= render partial: '/dmsf/path', locals: { folder: @folder, filename: nil, title: nil } %> + +<% if @projects.present? || @fast_links %> + <%= form_tag(entries_operations_dmsf_path, id: 'copyForm') do %> + <% @ids.each do |id| %> + <%= hidden_field_tag 'ids[]', id %> + <% end %> + <%= hidden_field_tag 'back_url', @back_url %> + <%= hidden_field_tag 'id', @project %> + <%= hidden_field_tag 'copy_entries', true %> + <%= hidden_field_tag 'move_entries', true %> +
+ <% unless @fast_links %> +

+ <%= label_tag 'dmsf_entries[target_project_id]', l(:field_target_project) %> + <%= select_tag 'dmsf_entries[target_project_id]', + project_tree_options_for_select(@projects, selected: @target_project) %> +

+ <% end %> +

+ <%= label_tag 'dmsf_entries[target_folder_id]', l(:field_target_folder) %><%= ' #' if @fast_links %> + <% if @fast_links %> + <%= text_field_tag 'dmsf_entries[target_folder_id]', '', required: true, max_length: 255 %> + <% else %> + <%= select_tag 'dmsf_entries[target_folder_id]', options_for_select(@folders, selected: @target_folder&.id) %> + <% end %> +

+
+

+ <%= submit_tag l(:button_copy), id: 'copy_button', data: { cy: "button__copy--dmsf" } %> + <%# 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" } %> + <% end %> +

+ <% end %> +<% end %> + +<%= javascript_tag do %> + $('#move_button').click(function(event) { + $('input#copy_entries').remove() + }); + <% unless @fast_links %> + $('#dmsf_entries_target_project_id').change(function () { + $('#content').load("<%= copymove_entries_path(id: @project, folder_id: @folder, ids: @ids) %>", + $('#copyForm').serialize()); + }); + $('#dmsf_entries_target_project_id').select2(); + $('#dmsf_entries_target_folder_id').select2(); + <% end %> +<% end %> + diff --git a/app/views/dmsf/create.api.rsb b/app/views/dmsf/create.api.rsb new file mode 100644 index 00000000..94caf8cc --- /dev/null +++ b/app/views/dmsf/create.api.rsb @@ -0,0 +1,4 @@ +api.dmsf_folder do + api.id @folder.id + api.title @folder.title +end \ No newline at end of file diff --git a/app/views/dmsf/digest.js.erb b/app/views/dmsf/digest.js.erb new file mode 100644 index 00000000..825876b0 --- /dev/null +++ b/app/views/dmsf/digest.js.erb @@ -0,0 +1,21 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +$('#ajax-modal').html('<%= escape_javascript render partial: 'digest' %>'); +showModal('ajax-modal', '30%', '<%= l(:label_dmsf_webdav_digest) %>'); diff --git a/app/views/dmsf/drop.js.erb b/app/views/dmsf/drop.js.erb new file mode 100644 index 00000000..ffbe121b --- /dev/null +++ b/app/views/dmsf/drop.js.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +throw new Error("<%= j flash[:error] %>"); diff --git a/app/views/dmsf/edit.html.erb b/app/views/dmsf/edit.html.erb new file mode 100644 index 00000000..f822f36a --- /dev/null +++ b/app/views/dmsf/edit.html.erb @@ -0,0 +1,128 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% html_title l(:dmsf) %> + +
+ <% if !@folder.new_record? && User.current.allowed_to?(:folder_manipulation, @project) && !@folder.system %> + <% if @folder.locked? %> + <%= link_to_if @folder.unlockable? && (!@folder.locked_for_user? || @force_file_unlock_allowed), + sprite_icon('unlock', l(:button_unlock)), + unlock_dmsf_path(id: @project, folder_id: @folder, + back_url: edit_dmsf_url(id: @project, folder_id: @folder)), + title: l(:title_unlock_file), class: 'icon icon-unlock' %> + <% else %> + <%= link_to sprite_icon('lock', l(:button_lock)), + lock_dmsf_path(id: @project, folder_id: @folder, + back_url: edit_dmsf_url(id: @project, folder_id: @folder)), + title: l(:title_lock_file), class: 'icon icon-lock' %> + <% end %> + <%= actions_dropdown do %> + <%= render partial: 'dmsf_context_menus/folder', + locals: { dmsf_folder: @folder, + locked: @folder.locked?, + allowed: true, + dmsf_link: nil, + project: @project, + folder: @folder.dmsf_folder, + edit: true, + unlockable: @folder.unlockable? && (!@folder.locked_for_user? || User.current.allowed_to?(:force_file_unlock, @project)), + email_allowed: User.current.allowed_to?(:email_documents, @project), + notifications: @notifications, + back_url: edit_dmsf_url(id: @project, folder_id: @folder) + } %> + <% end %> + <% end %> +
+ +<% create = @pathfolder == @parent %> +<%= render partial: 'path', + locals: { folder: @pathfolder, filename: create ? l(:heading_new_folder) : nil, title: nil } %> + +<%= labelled_form_for(@folder, + url: { action: create ? 'create' : 'save', id: @project, folder_id: @folder, parent_id: @parent }, + html: { method: :post }) do |f| %> + <%= error_messages_for @folder %> + <%= f.hidden_field :redirect_to_folder_id, value: @redirect_to_folder_id %> +
+

+ <%= f.text_field :title, required: true %> +

+

+ <%= f.text_area :description, rows: 8, class: 'wiki-edit dmsf-description' %> +

+

+ <% dir = @folder.inherited_permissions_from %> + <% if dir %> + <%= label_tag '', l(:label_inherited_permissions) %> + <% @project_roles.each do |role| %> + <% checked = dir.permission_for_role(role) %> + <% if checked %> + + <% end %> + <% end %> + +
+ <% users = dir.permissions_users %> + <% checkboxes = users_checkboxes(users, inherited: true) %> + <%= checkboxes %> +
+ <% if checkboxes.present? %> +
+ <% end %> + <% end %> + <%= label_tag '', l(:label_dmsf_permissions) %> + <% @project_roles.each do |role| %> + <% checked = @folder.permission_for_role(role) %> + + <% end %> + +
+ <% users = @folder.permissions_users %> + <% checkboxes = users_checkboxes(users) %> + <%= checkboxes %> +
+ <% if checkboxes.present? %> +
+ <% end %> + + <%= link_to sprite_icon('add', l(:label_search_for_watchers), size: 12), + new_dmsf_folder_permissions_path(project_id: @project, dmsf_folder_id: @folder), + remote: true, + method: :get %> + +

+ <% values = @folder ? @folder.custom_field_values : (@parent ? @parent.custom_field_values : DmsfFolder.new.custom_field_values) %> + <% values.each do |value| %> +

<%= custom_field_tag_with_label :dmsf_folder, value %>

+ <% end %> +
+
+ <%= submit_tag create ? l(:button_create) : l(:submit_save), class: 'button-positive', + data: { cy: "button__submit--dmsf_folder" } %> +
+<% end %> + +<%= wikitoolbar_for 'dmsf_folder_description' %> diff --git a/app/views/dmsf/edit_root.html.erb b/app/views/dmsf/edit_root.html.erb new file mode 100644 index 00000000..a3e93f62 --- /dev/null +++ b/app/views/dmsf/edit_root.html.erb @@ -0,0 +1,51 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% html_title(l(:dmsf)) %> + +
+ <% if @notifications && User.current.allowed_to?(:folder_manipulation, @project) %> + <% if @project.dmsf_notification %> + <%= link_to sprite_icon('email', l(:label_notifications_off)), + notify_deactivate_dmsf_path(id: @project, back_url: edit_root_dmsf_path(id: @project)), + title: l(:title_notifications_active_deactivate), + class: 'icon icon-email' %> + <% else %> + <%= link_to sprite_icon('email-disabled', l(:label_notifications_on)), + notify_activate_dmsf_path(id: @project, back_url: edit_root_dmsf_path(id: @project)), + title: l(:title_notifications_active_deactivate), + class: 'icon icon-email-add' %> + <% end %> + <% end %> +
+ +<%= render partial: 'path', locals: { folder: nil, filename: nil, title: nil } %> + +<%= labelled_form_for(@project, url: { action: 'save_root', id: @project }, html: { method: :post }) do |f| %> +
+

+ <%= f.text_area :dmsf_description, rows: 8, class: 'wiki-edit dmsf-description', label: l(:field_description) %> +

+
+ <%= f.submit l(:submit_save), class: 'button-positive' %> +
+
+<% end %> + +<%= wikitoolbar_for 'project_dmsf_description' %> diff --git a/app/views/dmsf/email_entries.html.erb b/app/views/dmsf/email_entries.html.erb new file mode 100644 index 00000000..1736b757 --- /dev/null +++ b/app/views/dmsf/email_entries.html.erb @@ -0,0 +1,74 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% html_title l(:dmsf) %> + +<%= render partial: 'path', locals: { folder: @folder, filename: nil, title: l(:heading_send_documents_by_email) } %> + +<%= form_tag(email_entries_path(id: @project, folder_id: @folder), { method: :post }) do %> + <%= hidden_field_tag 'email[zipped_content]', @email_params[:zipped_content] %> + <%= hidden_field_tag 'email[folders]', @email_params[:folders].to_json %> + <%= hidden_field_tag 'email[files]', @email_params[:files].to_json %> + <%= hidden_field_tag 'email[from]', @email_params[:from] %> + <%= hidden_field_tag 'email[reply_to]', @email_params[:reply_to] %> + <%= hidden_field_tag 'back_url', @back_url %> +
+

+ <%= label_tag '', l(:label_email_from) %> + <%= text_field_tag 'email[from_disabled]', @email_params[:from], class: 'dmsf-full-width', disabled: true %> +

+

+ <%= label_tag 'email[to]', l(:label_email_to) %> + + <%= text_field_tag 'email[to]', @email_params[:to], class: 'dmsf-full-width', required: true %> + <%= link_to sprite_icon('add', l(:button_add)), add_email_dmsf_path(id: @project), + title: l(:label_email_address_add), class: 'icon icon-add', remote: true %> + +

+

+ <%= label_tag 'email[cc]', l(:label_email_cc) %> + <%= text_field_tag 'email[cc]', @email_params[:cc], class: 'dmsf-full-width' %> +

+

+ <%= label_tag 'email[subject]', l(:label_email_subject) %> + <%= text_field_tag 'email[subject]', @email_params[:subject], class: 'dmsf-full-width' %> +

+

+ <%= label_tag '', l(:label_email_documents) %> + + <%= link_to 'Documents.zip', download_email_entries_path(id: @project, folder_id: @folder, entry: @email_params[:zipped_content]) %> + <%= l(:label_or) %> + <%= check_box_tag('email[links_only]', 1, RedmineDmsf.dmsf_documents_email_links_only?, + onchange: "$('#public_url').toggle($('#email_links_only').prop('checked'))") + %> + <%= l(:label_links_only) %> + <%= render partial: 'dmsf_public_urls/new' %> + +

+

+ <%= label_tag 'email[body]', l(:label_email_body) %> + <%= text_area_tag 'email[body]', @email_params['body'], rows: '20', class: 'dmsf-full-width wiki-edit' %> +

+
+ <%= submit_tag l(:label_email_send), class: 'button-positive' %> +
+
+<% end %> + +<%= wikitoolbar_for 'email_body' %> diff --git a/app/views/dmsf/index.html.erb b/app/views/dmsf/index.html.erb new file mode 100644 index 00000000..d3e11fc9 --- /dev/null +++ b/app/views/dmsf/index.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= render partial: 'main', locals: {} %> diff --git a/app/views/dmsf/query_rows.js.erb b/app/views/dmsf/query_rows.js.erb new file mode 100644 index 00000000..23076528 --- /dev/null +++ b/app/views/dmsf/query_rows.js.erb @@ -0,0 +1,23 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +// Add rows +$('#<%= params[:row_id] %>').after( + '<%= escape_javascript(render(partial: 'dmsf/query_rows', locals: { query: @query, dmsf_pages: nil })) %>' +); diff --git a/app/views/dmsf/save.api.rsb b/app/views/dmsf/save.api.rsb new file mode 100644 index 00000000..7af62e91 --- /dev/null +++ b/app/views/dmsf/save.api.rsb @@ -0,0 +1,5 @@ +api.dmsf_folder do + api.id @folder.id + api.title @folder.title + api.description @folder.description +end diff --git a/app/views/dmsf/show.api.rsb b/app/views/dmsf/show.api.rsb new file mode 100644 index 00000000..292cb07f --- /dev/null +++ b/app/views/dmsf/show.api.rsb @@ -0,0 +1,28 @@ +api.dmsf do + + api.array :dmsf_nodes, api_meta(total_count: @query.dmsf_nodes().count, limit: @limit, offset: @offset) do + @query.dmsf_nodes(offset: @offset, limit: @limit).each do |node| + api.node do + api.id node.id + api.title node.title + api.type node.type + case node.type + when 'file', 'url-link' + api.filename node.filename + when 'file-link', 'folder-link' + api.target_id node.revision_id.to_i + api.target_project_id node.project_id + end + end + end + end + + if @folder + api.found_folder do + api.id @folder.id + api.title @folder.title + render_api_custom_values @folder.visible_custom_field_values, api + end + end + +end diff --git a/app/views/dmsf/show.html.erb b/app/views/dmsf/show.html.erb new file mode 100644 index 00000000..d3e11fc9 --- /dev/null +++ b/app/views/dmsf/show.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= render partial: 'main', locals: {} %> diff --git a/app/views/dmsf/trash.html.erb b/app/views/dmsf/trash.html.erb new file mode 100644 index 00000000..1521afd5 --- /dev/null +++ b/app/views/dmsf/trash.html.erb @@ -0,0 +1,42 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% html_title l(:dmsf) %> + +
+ <% if @file_delete_allowed %> + <%= link_to sprite_icon('del', l(:label_empty_trash_bin)), empty_trash_path(id: @project), method: :delete %> + <% end %> +
+ +

<%= l(:link_trash_bin) %>

+ +<%= form_tag(trash_dmsf_path(id: @project), method: :get, id: 'query_form', class: 'dmsf-query-form') do %> +
+ <%= render partial: 'queries/query_form' %> +
+<% end %> +<%= render partial: 'query_list', locals: { query: @query, dmsf_pages: @dmsf_pages } %> +<%= pagination_links_full @dmsf_pages, @dmsf_count %> + +<%= context_menu %> + +<% content_for :sidebar do %> + <%= render partial: 'dmsf/sidebar' %> +<% end %> diff --git a/app/views/dmsf_context_menus/_approval_workflow.html.erb b/app/views/dmsf_context_menus/_approval_workflow.html.erb new file mode 100644 index 00000000..513881ff --- /dev/null +++ b/app/views/dmsf_context_menus/_approval_workflow.html.erb @@ -0,0 +1,82 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% workflows_available = DmsfWorkflow.where(['project_id = ? OR project_id IS NULL', project&.id]).exists? %> +<% wf = DmsfWorkflow.find_by(id: dmsf_file.last_revision.dmsf_workflow_id) if dmsf_file.last_revision.dmsf_workflow_id %> +<% file_approval_allowed = User.current.allowed_to?(:file_approval, project) %> +<% allowed = User.current && (dmsf_file.last_revision.dmsf_workflow_assigned_by_user == User.current) && wf %> +<% allowed_minor = dmsf_file.approval_allowed_zero_minor %> + +<% if file_approval_allowed %> + <% case dmsf_file.last_revision.workflow %> + <% when DmsfWorkflow::STATE_WAITING_FOR_APPROVAL %> + <% if wf %> + <% assignments = wf.next_assignments(dmsf_file.last_revision.id) %> + <% index = assignments.find_index{ |assignment| assignment.user_id == User.current.id } if assignments %> + <% assignment_id = (index && assignments && assignments[index]) ? assignments[index].id : nil %> + <% if allowed_minor %> + <%= context_menu_link sprite_icon('checked', l(:title_approval)), + action_dmsf_workflow_path(project_id: project.id, id: wf.id, + dmsf_workflow_step_assignment_id: assignment_id, + dmsf_file_revision_id: dmsf_file.last_revision.id, + back_url: back_url), + remote: true, class: 'icon icon-ok', id: 'dmsf-cm-workflow', + disabled: !assignments || !index %> + <% else %> + <%= context_menu_link sprite_icon('checked', l(:title_approval_minor)), + action_dmsf_workflow_path(project_id: project.id, id: wf.id, + dmsf_workflow_step_assignment_id: assignment_id, + dmsf_file_revision_id: dmsf_file.last_revision.id, + back_url: back_url), + remote: true, class: 'icon icon-ok', id: 'dmsf-cm-workflow', disabled: true %> + <% end %> + <% end %> + <% when DmsfWorkflow::STATE_ASSIGNED %> + <% if allowed_minor %> + <%= context_menu_link sprite_icon('checked', l(:title_start)), + start_dmsf_workflow_path(id: dmsf_file.last_revision.dmsf_workflow_id, + dmsf_file_revision_id: dmsf_file.last_revision.id, + back_url: back_url), + class: 'icon icon-ok', disabled: !allowed %> + <% else %> + <%= context_menu_link sprite_icon('checked', l(:title_start_minor)), + start_dmsf_workflow_path(id: dmsf_file.last_revision.dmsf_workflow_id, + dmsf_file_revision_id: dmsf_file.last_revision.id, + back_url: back_url), + class: 'icon icon-ok', disabled: true %> + <% end %> + <% when DmsfWorkflow::STATE_APPROVED, DmsfWorkflow::STATE_REJECTED, DmsfWorkflow::STATE_OBSOLETE %> + <%# %> + <% else %> + <% if allowed_minor %> + <%= context_menu_link sprite_icon('checked', l(:title_assignment)), + assign_dmsf_workflow_path(id: project.id, project_id: project.id, + dmsf_file_revision_id: dmsf_file.last_revision.id, + back_url: back_url), + remote: true, class: 'icon icon-ok', id: 'dmsf-cm-workflow', + disabled: locked || !workflows_available %> + <% else %> + <%= context_menu_link sprite_icon('checked', l(:title_assignment_minor)), + assign_dmsf_workflow_path(id: project.id, project_id: project.id, + dmsf_file_revision_id: dmsf_file.last_revision.id, + back_url: back_url), + remote: true, class: 'icon icon-ok', id: 'dmsf-cm-workflow', disabled: true %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/dmsf_context_menus/_file.html.erb b/app/views/dmsf_context_menus/_file.html.erb new file mode 100644 index 00000000..e0295785 --- /dev/null +++ b/app/views/dmsf_context_menus/_file.html.erb @@ -0,0 +1,116 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
  • + <%= 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) %> +
  • +<% unless dmsf_link %> +
  • + <%= 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}" } %> +
  • +
  • + <%= 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}" } %> +
  • +<% end %> +
  • + <% 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 %> + <% 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 %> + <% end %> +
  • +<% if notifications %> +
  • + <% 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 %> + <% 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 %> + <% end %> +
  • +<% end %> +
  • + <%= render partial: 'approval_workflow', locals: { dmsf_file: dmsf_file, project: project, locked: locked, + back_url: back_url } %> +
  • +
  • + <% 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 %> +
  • +
  • +<%= 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 %> +
  • +<% if RedmineDmsf.dmsf_webdav? %> +
  • + <% if dmsf_file.last_revision && dmsf_file.last_revision.protocol %> + <% url = "#{dmsf_file.last_revision.protocol}:ofe|u|#{Setting.protocol.strip}://#{Setting.host_name.strip}/dmsf/webdav/#{Addressable::URI.escape(RedmineDmsf::Webdav::ProjectResource.create_project_name(dmsf_file.project))}/" %> + <% if dmsf_file.dmsf_folder %> + <% url << "#{dmsf_file.dmsf_folder.dmsf_path_str}/" %> + <% end %> + <% url << dmsf_file.name %> + <% end %> + <% icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(dmsf_file.name)) %> + <%= context_menu_link sprite_icon(icon_name, l(:button_edit_content)), url, class: 'icon icon-file', + disabled: url.blank? || (locked && !unlockable) || + (Setting.plugin_redmine_dmsf['dmsf_webdav_strategy'] != 'WEBDAV_READ_WRITE') %> +
  • +<% end %> +
  • + <%= render partial: 'dmsf_context_menus/watch', locals: { object: dmsf_file } %> +
  • +<% if @preview %> +
  • + <%= context_menu_link sprite_icon('zoom-in', l(:label_preview)), view_dmsf_file_path(dmsf_file, preview: true), + class: 'icon icon-zoom-in', disabled: false %> +
  • +<% end %> +
  • + <%= context_menu_link sprite_icon('del', l(:button_delete)), + dmsf_link ? dmsf_link_path(id: dmsf_link, folder_id: folder, back_url: back_url) : dmsf_file_path(id: dmsf_file, + folder_id: folder, back_url: back_url), + method: :delete, class: 'icon icon-del', data: { confirm: l(:text_are_you_sure), + cy: "icon__delete--dmsf_file_#{dmsf_file.id}" }, id: 'dmsf-cm-delete', + disabled: !allowed || (locked && !dmsf_link) %> +
  • diff --git a/app/views/dmsf_context_menus/_file_trash.html.erb b/app/views/dmsf_context_menus/_file_trash.html.erb new file mode 100644 index 00000000..5f3110c4 --- /dev/null +++ b/app/views/dmsf_context_menus/_file_trash.html.erb @@ -0,0 +1,30 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
  • + <%= context_menu_link sprite_icon('reload', l(:title_restore)), + dmsf_link ? restore_dmsf_link_path(id: dmsf_link) : restore_dmsf_file_path(id: dmsf_file), + class: 'icon icon-cancel', disabled: !allowed_restore %> +
  • +
  • + <%= context_menu_link sprite_icon('del', l(:button_delete)), + dmsf_link ? dmsf_link_path(id: dmsf_link, commit: 'yes') : dmsf_file_path(id: dmsf_file, commit: 'yes'), + data: { confirm: l(:text_are_you_sure) }, method: :delete, class: 'icon icon-del', + id: 'dmsf-cm-delete', disabled: !allowed_delete %> +
  • diff --git a/app/views/dmsf_context_menus/_folder.html.erb b/app/views/dmsf_context_menus/_folder.html.erb new file mode 100644 index 00000000..f1effbb3 --- /dev/null +++ b/app/views/dmsf_context_menus/_folder.html.erb @@ -0,0 +1,104 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% unless edit %> +
  • + <%= context_menu_link sprite_icon('edit', l(:button_edit)), + edit_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url), + class: 'icon icon-edit', data: { cy: "icon__edit--dmsf_folder_#{dmsf_folder.id}" }, + disabled: !allowed || locked %> +
  • +<% end %> +<% unless dmsf_link %> +
  • + <%= 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 %> +
  • +
  • + <%= 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}" } %> +
  • +<% end %> +<% unless edit %> +
  • + <% if locked %> + <%= 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 %> + <% end %> +
  • +<% end %> +<% if notifications %> +
  • + <% if dmsf_folder.notification %> + <%= 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 %> +
  • +<% end %> +<% unless edit %> +
  • + <%= context_menu_link sprite_icon('download', l(:button_download)), + 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 %> +
  • +
  • + <%= context_menu_link sprite_icon('email', l(:field_mail)), + 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 %> +
  • +<% end %> +
  • + <%= render partial: 'dmsf_context_menus/watch', locals: { object: dmsf_folder } %> +
  • +
  • + <%= context_menu_link sprite_icon('del', l(:button_delete)), + dmsf_link ? dmsf_link_path(id: dmsf_link, folder_id: folder, back_url: back_url) : + delete_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, parent_id: folder, back_url: back_url), + data: { confirm: "#{l(:text_are_you_sure)}\n#{l(:text_not_empty) unless dmsf_folder.empty?}", + cy: "icon__delete--dmsf_folder_#{dmsf_folder.id}" }, method: :delete, + class: 'icon icon-del', id: 'dmsf-cm-delete', disabled: !allowed || (locked && !dmsf_link) %> +
  • diff --git a/app/views/dmsf_context_menus/_folder_trash.html.erb b/app/views/dmsf_context_menus/_folder_trash.html.erb new file mode 100644 index 00000000..1f6e2697 --- /dev/null +++ b/app/views/dmsf_context_menus/_folder_trash.html.erb @@ -0,0 +1,30 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
  • + <%= context_menu_link sprite_icon('reload', l(:title_restore)), + dmsf_link ? restore_dmsf_link_path(id: dmsf_link) : restore_dmsf_path(id: project, folder_id: dmsf_folder), + class: 'icon icon-cancel', disabled: !allowed_restore %> +
  • +
  • + <%= context_menu_link sprite_icon('del', l(:button_delete)), + dmsf_link ? dmsf_link_path(id: dmsf_link, commit: 'yes') : delete_dmsf_path(id: project, folder_id: dmsf_folder, commit: 'yes'), + data: { confirm: l(:text_are_you_sure) }, method: :delete, class: 'icon icon-del', + id: 'dmsf-cm-delete', disabled: !allowed_delete %> +
  • diff --git a/app/views/dmsf_context_menus/_main.html.erb b/app/views/dmsf_context_menus/_main.html.erb new file mode 100644 index 00000000..c1ba311e --- /dev/null +++ b/app/views/dmsf_context_menus/_main.html.erb @@ -0,0 +1,86 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% if folder_manipulation_allowed && !system_folder %> + <% if folder.nil? %> + <%= 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' } %> + <% 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' } %> + <% end %> + <% if folder && (!locked || User.current.allowed_to?(:force_file_unlock, project)) %> + <% if folder.locked? %> + <%= link_to_if folder.unlockable?, + 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' } %> + <% 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' } %> + <% end %> + <% end %> + <% if notifications && !locked %> + <% if ((folder && folder.notification) || (!folder && project.dmsf_notification)) %> + <%= 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' } %> + <% 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' } %> + <% end %> + <% end %> + <% if file_manipulation_allowed && !locked %> + <%= link_to sprite_icon('link', l(:label_link_from)), + 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' } %> + <% end %> +<% end %> +<%= render partial: 'dmsf_context_menus/watch', locals: { object: folder ? folder : project } %> +<% if trash_enabled %> + <%= 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' } %> +<% else %> + + <%= sprite_icon('del', l(:link_trash_bin)) %> + +<% end %> +<%= link_to sprite_icon('help', l(:label_help)), dmsf_help_path, class: 'icon icon-help', + onclick: "window.open('#{dmsf_help_url}','_blank', 'width=640,height=960'); return false;" %> diff --git a/app/views/dmsf_context_menus/_multiple.html.erb b/app/views/dmsf_context_menus/_multiple.html.erb new file mode 100644 index 00000000..00a20b7e --- /dev/null +++ b/app/views/dmsf_context_menus/_multiple.html.erb @@ -0,0 +1,43 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
  • + <%= context_menu_link sprite_icon('download', l(:button_download)), + entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids], + download_entries: true), + method: :post, class: 'icon icon-download', disabled: project.nil? %> +
  • +
  • + <%= context_menu_link sprite_icon('email', l(:field_mail)), + entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids], + email_entries: true), + method: :post, class: 'icon icon-email', disabled: !email_allowed %> +
  • +
  • + <%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"), + copymove_entries_path(id: project, folder_id: folder, ids: params[:ids], back_url: back_url), + class: 'icon icon-copy', disabled: project.nil? %> +
  • +
  • + <%= context_menu_link sprite_icon('del', l(:button_delete)), + entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids], + delete_entries: true), + method: :post, class: 'icon icon-del', data: { confirm: l(:text_are_you_sure) }, + id: 'dmsf-cm-delete', disabled: !allowed %> +
  • diff --git a/app/views/dmsf_context_menus/_multiple_trash.html.erb b/app/views/dmsf_context_menus/_multiple_trash.html.erb new file mode 100644 index 00000000..1bbc9e93 --- /dev/null +++ b/app/views/dmsf_context_menus/_multiple_trash.html.erb @@ -0,0 +1,32 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
  • + <%= context_menu_link sprite_icon('reload', l(:title_restore)), + entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids], + restore_entries: true), + method: :post, class: 'icon icon-cancel', disabled: !allowed_restore %> +
  • +
  • + <%= context_menu_link sprite_icon('del', l(:button_delete)), + entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids], + destroy_entries: true), + method: :post, class: 'icon icon-del', data: { confirm: l(:text_are_you_sure) }, + id: 'dmsf-cm-delete', disabled: !allowed_delete %> +
  • diff --git a/app/views/dmsf_context_menus/_project.html.erb b/app/views/dmsf_context_menus/_project.html.erb new file mode 100644 index 00000000..a563837d --- /dev/null +++ b/app/views/dmsf_context_menus/_project.html.erb @@ -0,0 +1,22 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
  • + <%= render partial: 'dmsf_context_menus/watch', locals: { object: dmsf_project } %> +
  • diff --git a/app/views/dmsf_context_menus/_revision_actions.html.erb b/app/views/dmsf_context_menus/_revision_actions.html.erb new file mode 100644 index 00000000..130e8737 --- /dev/null +++ b/app/views/dmsf_context_menus/_revision_actions.html.erb @@ -0,0 +1,37 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= link_to_function sprite_icon('group', l(:title_download_entries)), + "$('#revision_access_#{revision.id}').toggle(); $('.drdn.expanded').removeClass('expanded');", + class: 'icon icon-group dmsf-revision-action-button' %> +<% member = Member.find_by(user_id: User.current.id, project_id: revision.dmsf_file.project.id) %> +<% filename = revision.formatted_name(member) %> +<%= link_to sprite_icon('download', l(:button_download)), + static_dmsf_file_path(file, download: revision, filename: filename), + class: 'icon icon-download', disabled: false %> +<%= link_to sprite_icon('close', l(:title_obsolete_revision)), + obsolete_revision_path(revision), + data: { confirm: l(:text_are_you_sure) }, + class: 'icon icon-close dmsf-revision-action-button' if file_manipulation_allowed && (revision.workflow == DmsfWorkflow::STATE_APPROVED) %> +<% if file_delete_allowed && (file.dmsf_file_revisions.visible.count > 1) %> + <%= delete_link delete_revision_path(revision), + { data: { confirm: l(:text_are_you_sure) }, + title: l(:title_delete_revision), + class: 'icon icon-del dmsf-revision-action-button' } %> +<% end %> \ No newline at end of file diff --git a/app/views/dmsf_context_menus/_revisions.html.erb b/app/views/dmsf_context_menus/_revisions.html.erb new file mode 100644 index 00000000..add5ddc6 --- /dev/null +++ b/app/views/dmsf_context_menus/_revisions.html.erb @@ -0,0 +1,46 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% if notifications %> + <% if file.notification %> + <%= link_to sprite_icon('email', l(:label_notifications_off)), + notify_deactivate_dmsf_files_path(id: file, back_url: back_url), + title: l(:title_notifications_active_deactivate), + class: 'icon icon-email' %> + <% else %> + <%= link_to sprite_icon('email-disabled', l(:label_notifications_on)), + notify_activate_dmsf_files_path(id: file, back_url: back_url), + title: l(:title_notifications_not_active_activate), + class: 'icon icon-email-add' %> + <% end %> +<% end %> +<%= link_to sprite_icon('link', l(:label_link_to)), + new_dmsf_link_path(project_id: project.id, dmsf_folder_id: file.dmsf_folder ? file.dmsf_folder.id : nil, + dmsf_file_id: file.id, type: 'link_to', back_url: back_url), + title: l(:title_create_link), class: 'icon dmsf-icon-link' %> +<%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"), + copymove_entries_path(id: project, folder_id: file.dmsf_folder, ids: ["file-#{file.id}"], + back_url: back_url), class: 'icon icon-copy' %> +<% 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 %> +<%= 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 %> diff --git a/app/views/dmsf_context_menus/_url.html.erb b/app/views/dmsf_context_menus/_url.html.erb new file mode 100644 index 00000000..c3f4941e --- /dev/null +++ b/app/views/dmsf_context_menus/_url.html.erb @@ -0,0 +1,24 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
  • + <%= context_menu_link sprite_icon('del', l(:button_delete)), dmsf_link_path(dmsf_link), method: :delete, + class: 'icon icon-del', data: { confirm: l(:text_are_you_sure) }, id: 'dmsf-cm-delete', + disabled: !allowed %> +
  • diff --git a/app/views/dmsf_context_menus/_watch.html.erb b/app/views/dmsf_context_menus/_watch.html.erb new file mode 100644 index 00000000..8d4d6319 --- /dev/null +++ b/app/views/dmsf_context_menus/_watch.html.erb @@ -0,0 +1,26 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% watched = object.watched_by?(User.current) %> +<% css = [watcher_css([object]), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ') %> +<% icon = watched ? 'unwatch' : 'watch' %> +<% text = watched ? l(:button_unwatch) : l(:button_watch) %> +<% url = watch_path(object_type: object.class.to_s.underscore, object_id: object.id) %> +<% method = watched ? 'delete' : 'post' %> +<%= context_menu_link sprite_icon(icon, text), url, method: method, class: css, disabled: !User.current.logged? %> diff --git a/app/views/dmsf_context_menus/dmsf.html.erb b/app/views/dmsf_context_menus/dmsf.html.erb new file mode 100644 index 00000000..ba74aba8 --- /dev/null +++ b/app/views/dmsf_context_menus/dmsf.html.erb @@ -0,0 +1,48 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
      + <% if @dmsf_file %> + <%= render partial: 'file', locals: { project: @project, folder: @folder, dmsf_file: @dmsf_file, + dmsf_link: @dmsf_link, locked: @locked, unlockable: @unlockable, + allowed: @allowed, email_allowed: @email_allowed, + notifications: @notifications, preview: @preview, back_url: @back_url } %> + <% elsif @dmsf_folder %> + <%= render partial: 'folder', locals: { project: @project, folder: @folder, dmsf_folder: @dmsf_folder, + dmsf_link: @dmsf_link, locked: @locked, unlockable: @unlockable, + allowed: @allowed, email_allowed: @email_allowed, edit: false, + notifications: @notifications, back_url: @back_url } %> + <% elsif @dmsf_link %> + <%= render partial: 'url', locals: { dmsf_link: @dmsf_link, allowed: @allowed, back_url: @back_url } %> + <% elsif @dmsf_project %> + <%= render partial: 'project', locals: { dmsf_project: @dmsf_project, allowed: @allowed, back_url: @back_url } %> + <% else %> + <%= render partial: 'multiple', locals: { project: @project, folder: @folder, allowed: @allowed, + email_allowed: @email_allowed, back_url: @back_url } %> + <% end %> +
    + +<%= javascript_tag do %> + $('#dmsf-cm-delete').click(function (event) { + $('#context-menu').hide(); + }); + $('#dmsf-cm-workflow').click(function (event) { + $('#context-menu').hide(); + }); +<% end %> diff --git a/app/views/dmsf_context_menus/trash.html.erb b/app/views/dmsf_context_menus/trash.html.erb new file mode 100644 index 00000000..dd894cc6 --- /dev/null +++ b/app/views/dmsf_context_menus/trash.html.erb @@ -0,0 +1,38 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
      + <% if @dmsf_file || @dmsf_link %> + <%= render(partial: 'file_trash', locals: { project: @project, folder: @folder, dmsf_file: @dmsf_file, + dmsf_link: @dmsf_link, allowed_delete: @allowed_delete, + allowed_restore: @allowed_restore }) %> + <% elsif @dmsf_folder %> + <%= render(partial: 'folder_trash', locals: { project: @project, folder: @folder, dmsf_folder: @dmsf_folder, + dmsf_link: @dmsf_link, allowed_delete: @allowed_delete, + allowed_restore: @allowed_restore }) %> + <% else %> + <%= render(partial: 'multiple_trash', locals: { project: @project, folder: @folder, allowed_delete: @allowed_delete, + allowed_restore: @allowed_restore }) %> + <% end %> +
    +<%= javascript_tag do %> + $('#dmsf-cm-delete').click(function (event) { + $('#context-menu').hide(); + }); +<% end %> diff --git a/app/views/dmsf_files/_file_new_revision.html.erb b/app/views/dmsf_files/_file_new_revision.html.erb new file mode 100644 index 00000000..92b2f891 --- /dev/null +++ b/app/views/dmsf_files/_file_new_revision.html.erb @@ -0,0 +1,85 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +
    + <%= l(:heading_new_revision) %> + [+] + +
    + <% if @file.locked_for_user? %> +

    <%= l(:info_file_locked) %>

    + <% else %> + <%= labelled_form_for(@revision, url: { action: 'create_revision', id: @file }, + html: { method: :post, multipart: true, id: 'new_revision_form' }) do |f| %> + <%= hidden_field_tag 'back_url', params[:back_url] %> +
    +
    +

    + <%= f.text_field(:title) %> +

    +
    +
    +

    + <%= f.text_field :name, label: l(:label_file) %> +

    +
    +
    +

    + <%= f.text_area :description, rows: 6, class: 'wiki-edit dmsf-description' %> +

    +
    +
    +

    + <%= render partial: 'dmsf_files/version_selector', locals: { + label_tag_name: 'version', + select_tag_name_patch: 'version_patch', + select_tag_name_minor: 'version_minor', + select_tag_name_major: 'version_major', + revision_or_upload: @file.last_revision } %> +

    +
    +
    +
    + <% @revision.custom_field_values.each do |value| %> + <% value.value = nil if value.custom_field.dmsf_not_inheritable %> +

    <%= custom_field_tag_with_label(:dmsf_file_revision, value) %>

    + <% end %> +
    +
    +
    +

    + <%= label_tag 'file_upload', l(:label_new_content) %> + + <%= render partial: 'dmsf_upload/form', + locals: { multiple: false, container: nil, awf: false } %> + +

    +

    + <%= f.text_area :comment, rows: 2, label: l(:label_comment), class: 'wiki-edit dmsf-description' %> +

    +
    + <%= f.submit l(:button_create), class: 'button-positive', data: { cy: "button__submit--file_dmsf"} %> +
    + <% end %> + <% end %> +
    +
    + +<%= wikitoolbar_for 'dmsf_file_revision_description' %> +<%= wikitoolbar_for 'dmsf_file_revision_comment' %> diff --git a/app/views/dmsf_files/_link.html.erb b/app/views/dmsf_files/_link.html.erb new file mode 100644 index 00000000..7ae88222 --- /dev/null +++ b/app/views/dmsf_files/_link.html.erb @@ -0,0 +1,104 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% cls = link ? 'dmsf-gray' : '' %> + + + <% file_view_url = url_for({ controller: :dmsf_files, action: 'view', id: dmsf_file }) %> + <% icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(dmsf_file.name)) %> + <%= link_to sprite_icon(icon_name, h(link ? link.name : dmsf_file.title)), + file_view_url, + 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}" %> + + + (<%= number_to_human_size dmsf_file.last_revision.size %>) + <%= link_to '', view_dmsf_file_url(dmsf_file, download: dmsf_file.last_revision, disposition: 'attachment'), + title: l(:title_title_version_version_download, title: h(dmsf_file.last_revision.title), + version: dmsf_file.last_revision.version), class: 'icon icon-download dmsf-revision-action-button' %> + + + <%= dmsf_file.description unless dmsf_file.description.blank? %> + + + <%= dmsf_file.last_revision.user %>, <%= format_time(dmsf_file.last_revision.updated_at) %> + +<% # Command icons %> + + + <% # Details %> + <% if User.current.allowed_to? :file_manipulation, dmsf_file.project %> + <%= link_to sprite_icon('edit', ''), dmsf_file_path(id: dmsf_file, back_url: issue_path(@issue)), + title: l(:link_details, title: h(dmsf_file.last_revision.title)), + class: 'icon-only icon-edit' %> + <% else %> + + <% end %> + <% # Email %> + <%= link_to sprite_icon('email', ''), entries_operations_dmsf_path(id: dmsf_file.project_id, email_entries: 'email', + ids: ["file-#{dmsf_file.id}"], + back_url: issue_path(@issue)), + method: :post, title: l(:heading_send_documents_by_email), class: 'icon-only icon-email-disabled' %> + <% # Lock %> + <% if !dmsf_file.locked? %> + <%= link_to sprite_icon('lock', ''), lock_dmsf_files_path(id: dmsf_file, back_url: issue_path(@issue)), + title: l(:title_lock_file), class: 'icon-only icon-lock' %> + <% elsif dmsf_file.unlockable? && (!dmsf_file.locked_for_user? || User.current.allowed_to?(:force_file_unlock, dmsf_file.project)) %> + <%= link_to sprite_icon('unlock', ''), unlock_dmsf_files_path(id: dmsf_file, back_url: issue_path(@issue)), + title: dmsf_file.locked_title, class: 'icon-only icon-unlock' %> + <% else %> + <%= content_tag('span', sprite_icon('unlock', ''), title: dmsf_file.locked_title) %> + <% end %> + <% if !dmsf_file.locked? %> + <% # Notifications %> + <% if dmsf_file.notification %> + <%= link_to sprite_icon('email', ''), notify_deactivate_dmsf_files_path(id: dmsf_file, + back_url: issue_path(@issue)), + title: l(:title_notifications_active_deactivate), class: 'icon-only icon-email' %> + <% else %> + <%= link_to sprite_icon('email-disabled', ''), notify_activate_dmsf_files_path(id: dmsf_file, + back_url: issue_path(@issue)), + title: l(:title_notifications_not_active_activate), class: 'icon-only icon-email-add' %> + <% end %> + <% else %> + + + <% end %> + <% # Delete %> + <% if @issue.attributes_editable? && + ((link && User.current.allowed_to?(:file_manipulation, dmsf_file.project)) || + (!link && User.current.allowed_to?(:file_delete, dmsf_file.project))) %> + <% url = if link + dmsf_link_path(link, commit: 'yes', back_url: issue_path(@issue)) + else + dmsf_file_path(id: dmsf_file, commit: 'yes', back_url: issue_path(@issue)) + end %> + <%= delete_link url, {}, '' %> + <% end %> + <% # Approval workflow %> + <% wf = DmsfWorkflow.find_by(id: dmsf_file.last_revision.dmsf_workflow_id) if dmsf_file.last_revision.dmsf_workflow_id %> + <%= render partial: 'dmsf_workflows/approval_workflow_button', + locals: { file: dmsf_file, + file_approval_allowed: User.current.allowed_to?(:file_approval, dmsf_file.project), + workflows_available: DmsfWorkflow.where(['project_id = ? OR project_id IS NULL', dmsf_file.project_id]).exists?, + project: dmsf_file.project, wf: wf, dmsf_link_id: nil, back_url: issue_path(@issue) } %> + + diff --git a/app/views/dmsf_files/_links.html.erb b/app/views/dmsf_files/_links.html.erb new file mode 100644 index 00000000..065348b2 --- /dev/null +++ b/app/views/dmsf_files/_links.html.erb @@ -0,0 +1,36 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% if links.present? %> +
    +

    + <%= l(:menu_dmsf) %> +

    +
    + <% # DMS documents & links %> + + <% links.each do |dmsf_file, link, _| %> + + <%= render partial: 'dmsf_files/link', locals: { dmsf_file: dmsf_file, link: link } %> + + <% end %> +
    + <%= render partial: 'dmsf_files/thumbnails', locals: { links: links, thumbnails: thumbnails, link_to: true } %> +
    +<% end %> diff --git a/app/views/dmsf_files/_revision_access.html.erb b/app/views/dmsf_files/_revision_access.html.erb new file mode 100644 index 00000000..47dab1ac --- /dev/null +++ b/app/views/dmsf_files/_revision_access.html.erb @@ -0,0 +1,41 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% query_options = nil unless defined?(query_options) %> +<% query_options ||= {} %> +
    + + + + <% query.inline_columns.each do |column| %> + <%= column_header query, column, query_options %> + <% end %> + + + + <% query.accesses.each do |access| %> + + <% query.inline_columns.each do |column| %> + <%= content_tag 'td', column_content(column, access), class: column.css_classes %> + <% end %> + + <% end %> + +
    +
    diff --git a/app/views/dmsf_files/_thumbnails.html.erb b/app/views/dmsf_files/_thumbnails.html.erb new file mode 100644 index 00000000..3ffb90c9 --- /dev/null +++ b/app/views/dmsf_files/_thumbnails.html.erb @@ -0,0 +1,41 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% # Thumbnails %> +<% if defined?(thumbnails) && thumbnails %> + <% images = links.map{ |x| x[0] }.select(&:thumbnailable?) %> + <% if images.any? %> + <% if link_to # Redmine classic %> +
    + <% end %> + <% images.each do |file| %> +
    + <% if link_to # Redmine classic %> + <%= link_to image_tag(dmsf_thumbnail_path(file), alt: file.title), view_dmsf_file_url(file) %> + <% else # jQuery gallery %> + <%= image_tag(dmsf_thumbnail_path(file), + { :'data-fullsrc' => view_dmsf_file_url(file), alt: file.title }) %> + <% end %> +
    + <% end %> + <% if link_to # Redmine classic %> +
    + <% end %> + <% end %> +<% end %> diff --git a/app/views/dmsf_files/_version_selector.html.erb b/app/views/dmsf_files/_version_selector.html.erb new file mode 100644 index 00000000..90531107 --- /dev/null +++ b/app/views/dmsf_files/_version_selector.html.erb @@ -0,0 +1,45 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= label_tag(label_tag_name, l(:label_dmsf_version)) %> +<%= select_tag select_tag_name_major, options_for_select(DmsfUploadHelper::major_version_select_options, + DmsfUploadHelper::gui_version(revision_or_upload.major_version)), + class: 'dmsf-select-version' %> +. +<% if revision_or_upload.patch_version.present? && (DmsfUploadHelper::gui_version(revision_or_upload.patch_version) != ' ') %> + <%= select_tag select_tag_name_minor, options_for_select(DmsfUploadHelper::minor_version_select_options, + DmsfUploadHelper::gui_version(revision_or_upload.minor_version)), + class: 'dmsf-select-version' %> + . + <%= select_tag select_tag_name_patch, options_for_select(DmsfUploadHelper::patch_version_select_options, + DmsfUploadHelper::gui_version(DmsfUploadHelper.increase_version(revision_or_upload.patch_version))), + class: 'dmsf-select-version' %> +<% else %> + <% if revision_or_upload.minor_version %> + <% minor_version = DmsfUploadHelper::gui_version(DmsfUploadHelper.increase_version(revision_or_upload.minor_version)) %> + <% else %> + <% minor_version = '' %> + <% end %> + <%= select_tag select_tag_name_minor, + options_for_select(DmsfUploadHelper::minor_version_select_options, minor_version), + class: 'dmsf-select-version' %> + . + <%= select_tag select_tag_name_patch, options_for_select(DmsfUploadHelper::patch_version_select_options, ' '), + class: 'dmsf-select-version' %> +<% end %> diff --git a/app/views/dmsf_files/create_revision.api.rsb b/app/views/dmsf_files/create_revision.api.rsb new file mode 100644 index 00000000..bd7e2182 --- /dev/null +++ b/app/views/dmsf_files/create_revision.api.rsb @@ -0,0 +1,3 @@ +api.dmsf_file_revision do + api.id @file.last_revision&.id +end \ No newline at end of file diff --git a/app/views/dmsf_files/document.html.erb b/app/views/dmsf_files/document.html.erb new file mode 100644 index 00000000..92ac8844 --- /dev/null +++ b/app/views/dmsf_files/document.html.erb @@ -0,0 +1,22 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= render layout: 'layouts/document' do %>  + <%= render_document_content @file, @content %> +<% end %> diff --git a/app/views/dmsf_files/show.api.rsb b/app/views/dmsf_files/show.api.rsb new file mode 100644 index 00000000..80feeb9e --- /dev/null +++ b/app/views/dmsf_files/show.api.rsb @@ -0,0 +1,43 @@ +api.dmsf_file do + api.id @file.id + api.title @file.title + api.name @file.name + api.project_id @file.project_id + api.dmsf_folder_id @file.dmsf_folder_id if @file.dmsf_folder_id + api.content_url download_dmsf_file_url(@file) + api.array :dmsf_file_revisions do + @file.dmsf_file_revisions.each do |r| + api.dmsf_file_revision do + api.id r.id + api.source_dmsf_file_revision_id r.source_dmsf_file_revision_id + api.name r.name + 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.title r.title + api.description r.description + api.workflow r.workflow + if r.patch_version.present? + api.version "#{r.major_version}.#{r.minor_version}.#{r.patch_version}" + elsif r.minor_version.present? + api.version "#{r.major_version}.#{r.minor_version}" + else + api.version r.major_version + end + api.comment r.comment + api.user_id r.user_id + api.created_at r.created_at + api.updated_at r.updated_at + api.dmsf_workflow_id r.dmsf_workflow_id + api.dmsf_workflow_assigned_by_user_id r.dmsf_workflow_assigned_by_user_id + api.dmsf_workflow_assigned_at r.dmsf_workflow_assigned_at + 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 + render_api_custom_values r.visible_custom_field_values, api + end + end + end +end diff --git a/app/views/dmsf_files/show.html.erb b/app/views/dmsf_files/show.html.erb new file mode 100644 index 00000000..f8138ba2 --- /dev/null +++ b/app/views/dmsf_files/show.html.erb @@ -0,0 +1,184 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% html_title l(:dmsf) %> + +
    + <% if @file_manipulation_allowed %> + <% if @file.locked_for_user? %> + <% if User.current.allowed_to?(:force_file_unlock, @project) %> + <%= link_to_if @file.unlockable?, sprite_icon('unlock', l(:button_unlock)), + unlock_dmsf_files_path(id: @file, back_url: dmsf_file_path(id: @file)), + title: l(:title_unlock_file), class: 'icon icon-unlock' %> + <% end %> + <% else %> + <% if @file.locked? %> + <%= link_to_if @file.unlockable?, sprite_icon('unlock', l(:button_unlock)), + unlock_dmsf_files_path(id: @file, back_url: dmsf_file_path(id: @file)), + title: l(:title_unlock_file), class: 'icon icon-unlock' %> + <% else %> + <%= link_to sprite_icon('lock', l(:button_lock)), + lock_dmsf_files_path(id: @file, back_url: dmsf_file_path(id: @file)), + title: l(:title_lock_file), class: 'icon icon-lock' %> + <% end %> + <%= actions_dropdown do %> + <%= render partial: 'dmsf_context_menus/revisions', + locals: { project: @project, file: @file, file_delete_allowed: @file_delete_allowed, + notifications: @notifications, back_url: dmsf_file_path(id: @file) } %> + <% end %> + <% end %> + <% end %> +
    + +<%= render partial: '/dmsf/path', locals: { folder: @file.dmsf_folder, filename: @file.title, title: nil } %> + +<% if @file_manipulation_allowed && !@file.locked_for_user? %> + <%= error_messages_for 'file' %> + <%= error_messages_for 'revision' %> + <%= render partial: 'file_new_revision' %> +<% end %> + +
    +
    + <%= label_tag '', l(:label_document) %> + #<%= @file.id %> +
    +
    + +

    <%= l(:heading_revisions) %>

    +<% @file.dmsf_file_revisions.visible[@revision_pages.offset, @revision_pages.per_page].each do |revision| %> +
    +
    +
    + <%= actions_dropdown do %> + <%= render partial: 'dmsf_context_menus/revision_actions', + locals: { project: @project, file: @file, file_delete_allowed: @file_delete_allowed, + file_manipulation_allowed: @file_manipulation_allowed, revision: revision } %> + <% end %> +
    +
    + <%= l(:info_revision, rev: revision.id) %> + <%= (revision.source_revision.nil? ? l(:label_created) : l(:label_changed)).downcase %> + <%= l(:info_changed_by_user, changed: format_time(revision.updated_at)) %> + <%= link_to(revision.user.name, user_path(revision.user)) if revision.user %> +
    +
    +
    +
    +
    +
    + <%= content_tag :div, l(:label_title), class: 'label' %> + <%= content_tag :div, h(revision.title), class: 'value' %> +
    + <% if revision.description.present? %> +
    + <%= content_tag :div, l(:label_description), class: 'label' %> + <% text = clean_wiki_text(textilizable(revision.description)) %> + <%= content_tag :div, text.html_safe, class: 'value wiki' %> +
    + <% end %> +
    + <%= content_tag :div, l(:label_dmsf_version), class: 'label' %> + <%= content_tag :div, "#{revision.version}", class: 'value' %> +
    +
    + <%= content_tag :div, l(:label_size), class: 'label' %> + <%= content_tag :div, number_to_human_size(revision.size), class: 'value' %> +
    + <% wf = DmsfWorkflow.find_by(id: revision.dmsf_workflow_id) %> + <% if wf %> +
    + <%= content_tag :div, l(:label_workflow), class: 'label' %> +
    + <%= "#{wf.name} - " %> + <%= link_to(revision.workflow_str(false), + log_dmsf_workflow_path(project_id: @project.id, + id: wf.id, dmsf_file_revision_id: revision.id), + title: revision.workflow_tooltip, + remote: true) %> +
    +
    + <% end %> + <% if revision.comment.present? %> +
    + <%= content_tag :div, l(:label_comment), class: 'label' %> + <% text = clean_wiki_text(textilizable(revision.comment)) %> + <%= content_tag :div, text.html_safe, class: 'value wiki' %> +
    + <% end %> +
    +
    +
    + <%= content_tag :div, l(:label_file), class: 'label' %> +
    + <% path = "#{revision.dmsf_file.dmsf_folder.dmsf_path_str}/" if revision.dmsf_file.dmsf_folder %> + <%= h("#{path}#{revision.name}") %> +
    +
    +
    + <%= content_tag :div, l(:label_mime), class: 'label' %> + <%= content_tag :div, revision.mime_type, class: 'value' %> +
    + <% if revision.digest.present? %> +
    + <%= content_tag :div, l(:field_digest), class: 'label' %> + <%= content_tag :div, "#{revision.digest_type}: #{revision.digest}", class: 'value wiki' %> +
    + <% end %> + <%= render 'dmsf/custom_fields', object: revision %> +
    +
    +
    "> + <% if @file_manipulation_allowed %> + <% revision_access_query = DmsfFileRevisionAccessQuery.new %> + <% revision_access_query.revision_id = revision.id %> + <%= render partial: 'revision_access', locals: { revision: revision, query: revision_access_query } %> + <% end %> +
    +
    +
    +<% end %> +<%= pagination_links_full @revision_pages, @revision_count %> + +<% if @file.watchers.present? && User.current.allowed_to?(:view_dmsf_file_watchers, @project) %> + <% content_for :sidebar do %> +
    + <%= render partial: 'watchers/watchers', locals: { watched: @file } %> +
    + <% end %> +<% end %> + +<%= javascript_tag do %> + $('a.delete-revision').click(function(event) { + if(!window.confirm('<%= l(:text_are_you_sure) %>')) { + event.preventDefault(); + } + }); + $('a.delete-entry').click(function(event) { + if(!window.confirm('<%= l(:text_are_you_sure) %>')) { + event.preventDefault(); + } + }); + $('#new_revision_form_content_toggle').click(function() { + let newRevisionForm = $('#new_revision_form_content'); + let operator = newRevisionForm.is(':visible') ? '+' : '-'; + $(this).text('[' + operator + ']'); + newRevisionForm.toggle(); + }); +<% end %> diff --git a/app/views/dmsf_folder_permissions/_new.html.erb b/app/views/dmsf_folder_permissions/_new.html.erb new file mode 100644 index 00000000..7c5b6721 --- /dev/null +++ b/app/views/dmsf_folder_permissions/_new.html.erb @@ -0,0 +1,34 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

    <%= l(:label_dmsf_permissions) %>

    +<%= form_tag(append_dmsf_folder_permissions_path, remote: true, method: :post, id: 'new-user-form') do %> + <%= hidden_field_tag :project_id, @project.id %> +

    <%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %>

    + <%= javascript_tag "observeSearchfield('user_search', 'users_for_users', '#{ escape_javascript url_for( + controller: 'dmsf_folder_permissions', action: 'autocomplete_for_user', project_id: @project, + dmsf_folder_id: @dmsf_folder) }')" %> +
    + <%= render_principals_for_new_folder_permissions @principals %> +
    +
    + <%= submit_tag l(:button_add), onclick: 'hideModal(this);', class: 'button-positive' %> + <%= link_to_function l(:button_cancel), "hideModal(this);" %> +
    +<% end %> diff --git a/app/views/dmsf_folder_permissions/append.js.erb b/app/views/dmsf_folder_permissions/append.js.erb new file mode 100644 index 00000000..ce4f04f8 --- /dev/null +++ b/app/views/dmsf_folder_permissions/append.js.erb @@ -0,0 +1,24 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +$('#users_for_watcher').html('<%= escape_javascript(render_principals_for_new_folder_permissions(@principals)) %>'); +<% @principals.each do |principal| %> + $("#user_permission_ids_<%= principal.id %>").remove(); +<% end %> +$('#user_permissions').append('<%= escape_javascript(users_checkboxes(@principals)) %>'); diff --git a/app/views/dmsf_folder_permissions/autocomplete_for_user.js.erb b/app/views/dmsf_folder_permissions/autocomplete_for_user.js.erb new file mode 100644 index 00000000..29dc6e60 --- /dev/null +++ b/app/views/dmsf_folder_permissions/autocomplete_for_user.js.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +$('#users_for_watcher').html('<%= escape_javascript(render_principals_for_new_folder_permissions(@principals)) %>'); diff --git a/app/views/dmsf_folder_permissions/new.js.erb b/app/views/dmsf_folder_permissions/new.js.erb new file mode 100644 index 00000000..27c7b8a3 --- /dev/null +++ b/app/views/dmsf_folder_permissions/new.js.erb @@ -0,0 +1,25 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +var modal = $('#ajax-modal'); + +$('#users_for_watcher').html('<%= escape_javascript(render_principals_for_new_folder_permissions(@principals)) %>'); +modal.html('<%= escape_javascript(render partial: 'dmsf_folder_permissions/new') %>'); +showModal('ajax-modal', '400px'); +modal.addClass('new-user'); diff --git a/app/views/dmsf_help/cs/wiki_syntax.html b/app/views/dmsf_help/cs/wiki_syntax.html new file mode 100644 index 00000000..f7703722 --- /dev/null +++ b/app/views/dmsf_help/cs/wiki_syntax.html @@ -0,0 +1,48 @@ + + + + +Wiki formatting + + + + +

    Referenční dokumentace syntaxe DMS

    + +

    DMS přidává následující makra:

    + +
    +
    Dokumenty
    +
    +

    {{dmsf(17)}} - odkaz na dokument #17

    +

    {{dmsf(17, File)}} - odkaz na dokument #17 s textem "File"

    +

    {{dmsf(17, File, 10)}} - odkaz na dokument #17 r10 s textem "File"

    +

    {{dmsfd(17)}} - odkaz na detaily dokument #17

    +

    {{dmsfdesc(17)}} - odkaz na popis dokumentu #17

    +
    +
    Složky
    +
    +

    {{dmsff(5)}} - odkaz na složku #5

    +

    {{dmsff(5, Folder)}} - odkaz na složku #5 s textem "Folder"

    +
    +
    Obrázky
    +
    +

    {{dmsf_image(8)}} - vložený obrázek dokumentu #8 (dokument musí být obrázek typu JPEG, PNG,...)

    +

    {{dmsf_image(8, size=300)}}

    +

    {{dmsf_image(8, size=640x480)}}

    +

    {{dmsf_image(8, size=50%)}}

    +

    {{dmsf_image(8, height=300)}}

    +

    {{dmsf_image(8, width=300)}} - obrázek dokumentu #8 s nastavenou velikostí

    +

    {{dmsftn(8)}} - náhled obrázku dokumentu #8

    +

    {{dmsftn(8, size=300)}} - náhled obrázku dokumentu #8 s nastavenou velikostí

    +
    +
    Schvalovací procesy
    +
    +

    {{dmsfw(8)}} - schvalovací proces dokumentu #8

    +
    +
    + +ID dokumentu nebo složky naleznete v jejích detailech. + + + \ No newline at end of file diff --git a/app/views/dmsf_help/de/wiki_syntax.html b/app/views/dmsf_help/de/wiki_syntax.html new file mode 100644 index 00000000..2016c9de --- /dev/null +++ b/app/views/dmsf_help/de/wiki_syntax.html @@ -0,0 +1,48 @@ + + + + +Wiki formatting + + + + +

    DMS Syntax Reference

    + +

    DMS fügt die folgenden integrierten Makros hinzu:

    + +
    +
    Dokumente
    +
    +

    {{dmsf(17)}} - Link zum Dokument #17

    +

    {{dmsf(17, File)}} - Link zum Dokument #17 mit dem Text "File"

    +

    {{dmsf(17, File, 10)}} - Link zum Dokument #17 r10 mit dem Text "File"

    +

    {{dmsfd(17)}} - Link zu den Dokumentdetails #17

    +

    {{dmsfdesc(17)}} - Link zur Beschreibung des Dokuments #17

    +
    +
    Ordner
    +
    +

    {{dmsff(5)}} - Link zum Ordner #5

    +

    {{dmsff(5, Folder)}} - Link yum Ordner #5 mit dem Text "Folder"

    +
    +
    Bilder
    +
    +

    {{dmsf_image(8)}} - Bild des Dokuments #8 (der Dokumnt must eine Bilddatei wie JPEG, PNG,... sein

    +

    {{dmsf_image(8, size=300)}}

    +

    {{dmsf_image(8, size=640x480)}}

    +

    {{dmsf_image(8, size=50%)}}

    +

    {{dmsf_image(8, height=300)}}

    +

    {{dmsf_image(8, width=300)}} - Bild des Dokuments #8 mit Benutzerdefinierte Größe

    +

    {{dmsftn(8)}} - Miniaturansicht des Documents #8

    +

    {{dmsftn(8, size=300)}} - Miniaturansicht des Documents #8 mit Benutzerdefinierte Größe

    +
    +
    Genehmigungs-Workflows
    +
    +

    {{dmsfw(8)}} - Workflowstatus des Dokuments #8

    +
    +
    + +ID des Dokuments/Ordners man kann in sein Details finden. + + + \ No newline at end of file diff --git a/app/views/dmsf_help/en/dmsf_help.html.erb b/app/views/dmsf_help/en/dmsf_help.html.erb new file mode 100644 index 00000000..754e2846 --- /dev/null +++ b/app/views/dmsf_help/en/dmsf_help.html.erb @@ -0,0 +1,792 @@ + + + + + Wiki formatting + <%= stylesheet_link_tag 'wiki_syntax.css' %> + <%= stylesheet_link_tag 'dmsf_help.css', plugin: :redmine_dmsf %> + + + +

    DMSF User's guide

    + +

    CONTENTS

    + + +
    +

    1 Introduction

    +

    + Document Management System “Features” - DMSF is a 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. +

    +

    + Initial development was for Kontron AG R&D department and it is released as open source thanks to their + generosity. +

    +

    + Redmine Document Management System "Features" plugin is distributed under GNU General Public License v2 (GPL). +

    +

    + Redmine is a flexible project management web application, released under the terms of the GNU General Public + License v2 (GPL) at http://www.redmine.org +

    +

    + Further information about the GPL license can be found at + + http://www.gnu.org/licenses/old-licenses/gpl-2.0.html#SEC1 +

    +
    + Syntax +

    In this document the following syntax is used:

    + Italics – name of a company or product
    + Bold italics – links, click-able items
    + Bold – names of tabs, or fields you can fill in
    + Courier – commands +
    +
    + +
    +

    2 Features

    +
      +
    • Directory structure
    • +
    • Document versioning / revision history
    • +
    • Email notifications for directories and/or documents
    • +
    • Document locking
    • +
    • Multi (drag/drop depending on browser) upload/download
    • +
    • Multi download via zip
    • +
    • Direct document or document link sending via email
    • +
    • Configurable document approval workflow
    • +
    • Document access auditing
    • +
    • Integration with Redmine's activity feed
    • +
    • Wiki macros for quick content linking
    • +
    • Full read/write WebDAV functionality
    • +
    • Fulltext search
    • +
    • Documents and files symbolic links
    • +
    • Custom fields
    • +
    • Trash bin
    • +
    • Documents preview
    • +
    +
    + +
    +

    3 My Account

    +

    + The following options related to DMS are available in My account: +

    +
      +
    • Default DMS query - The default query which will be default in the DMS view
    • +
    • Receive download notifications - You will receive download notifications for watched documents
    • +
    +
    + +
    +

    4 Project settings

    +
    + +
    +

    4.1 Modules

    +

    + In order to be DMS available, it is necessary to have checked the DMS module on the Modules tab. +

    +
    + +
    +

    4.2 DMS

    +

    + Every project member is allowed to customize the DMS module behavior as follows: +

    + DMS settings +

    + Your DMS project's preferences +

    +

    + Notifications – Activated(default), Deactivated – If deactivated, you won't receive any email + notifications related to documents and vice versa. +

    +

    + Title format – File names of downloaded documents can be formated using following tags: +

    +
      +
    • %t – document title
    • +
    • %d – the date when the document was uploaded
    • +
    • %v – version of the documentation
    • +
    • %i – ID number of the document
    • +
    • %r – revision number of the document
    • +
    +

    + Fast links – If selected, you are expected to enter a document’s ID instead of selecting the document + from a pick list when creating a document link. +

    +
    + Act as attachable +
      +
    • Files & Documents – You can attach Files and Document to issues.
    • +
    • Files – You can attach Files only to issues.
    • +
    +

    + This feature provides a possibility to attach documents to issues. The new/edit issue form contains a + Drag&Drop area for uploading documents. It’s also possible to link existing documents using the button Link + from. Attached documents are stored in system folders visible from the user interface depending on + permissions. +

    +
    + DMS attachments +

    + Default query - You can select an existing query which will be default in DMS view. +

    +
    + +
    +

    4.3 Approval workflows

    +

    + Definition of approval workflows of the project. +

    + Approval workflows +
    +

    + This tab allows project managers to define approval workflows. +

    +
      +
    • + The approval workflow feature as a part of Document Management System allowing users to create an + approval chain for document approving +
    • +
    • + An approval workflow is defined by a logical name and by assigned users – approvers in a particular + order with a given dependency +
    • +
    • Each approval workflow consists of one or more approval steps
    • +
    • + Each approval step includes one or more approver and their dependency. It means that we can assign + a new user with a dependency AND or OR. So to proceed to a next approval step it is required an approval + by one or all approvers. Consequently we are able to define serial or parallel workflow this way +
    • +
    +
    +

    + To create a new approval workflow click on the icon New approval workflowin the top left corner. + Then you are expected to enter a name of the new approval wokflow: +

    + New aproval workflows +

    Now fill in a new workflow name and click on Create button.

    +

    Optionally you can select an existing approval workflow to be copied.

    +

    + Just created workflow appears on the workflow list. In order to change the workflow name or to define workflow + steps click on the workflow name. In our case on the text Basic. +

    + Basic +

    + Then workflow update form appears. In the top of the form you can update the workflow name. Edit the name there + and click on Save button. +

    +

    + Using the New step button you can choose one or more approvals and by clicking on AND + or OR button you will add a new approval step to the end or by choosing an existing step from + the list to an existing step. +

    +

    + Later on exiting approval steps can be removed, reordered or edited by clicking on corresponding icon next to + the step approvals. +

    +
    + +
    +

    5 DMS module

    +

    + The DMS module allows you to store all documentation relevant to a project at one place, sorted + into folders. You can switch on the Documents module by going to the project Settings → Modules tab. Check the check box in front of the DMS item, which is the Document Management System Feature. +

    +
    + +
    +

    5.1 Documents management

    +

    + The documents are stored in folders, which should have such a structure that provides an easy orientation within the topic. +

    + DMS Main +
    + +
    +

    5.2 New folder

    +

    + You can create a new folder by clicking the green plus sign Create folder in the right upper corner. This + takes you to the following screen: +

    + New folder +

    + Type the new folder Title and click the Create button. You can add a description of the + folder. You can use various types of text formatting, such as headings, lists, links to web pages, documents + etc. If you need help with the text formatting, click the Helpb> tool button and you are provided + with a list of commands, including examples. Moreover, you can set folder's custom fields if any. Access to the + folder can be driven by selecting certain roles or particular users in Allow access only to section. +

    + Wiki help +

    + A new folder can be also created by copying an existing folder. Go to the folder you wish to create a + copy of, and click the Edit icon: +

    + Edit folder +

    + Or click on Edit item in the context menu next to the folder in the main view: +

    + Edit folder 2 +

    + On the folder edit page choose Copy/Move from the context menu: +

    + Folder Copy/Move +

    + Select the Target project and Target folder. Click the Copy button. +

    +
    + +
    +

    5.3 Folder permissions

    +

    + In the new folder form or later in the edit form you can specify an extra folder permissions. By selecting roles + and members you can control access to the folder. +

    + Folder's permissions +

    + Only the selected roles or users will have access to the folder. +

    +
    + +
    +

    5.4 List of folders

    + Each line of the folder list contains: +
      +
    • folder's name
    • +
    • folder's date of creation and author
    • +
    • folder's context menu
    • +
    +

    + The editing tool uses the same form as is used for creating a new folder. You can change the folder's title, + description and permissions. +

    +

    + The list of folders can by ordered by any column. Just click on the column title and the list will be reordered by + the column's values. If you click the same title again the order will be reversed. +

    + Ordering folders +
    + +
    +

    5.5 Filtering

    +

    + You can limit the view to certain folders and documents by defining filter rules. An empty filter for the title + is displayed by default. Additionally, you can add more filters using the pick list on the right side. In case + of a list of values you can combine multiple values. +

    + Filtering +
    + +
    +

    5.6 Options

    +

    + There in Options you can specify visible columns. +

    + Options +
    + +
    +

    5.7 Custom queries

    +

    + Defined filters and options can be saved as custom queries for later usage. +

    +

    + Firstly, define filters and options. Then, save them as a custom query using the link Save custom query. +

    +

    + Defined custom queries can be then edited, cleared or deleted using corresponding commands: + Edit custom query, Clear and Delete custom query. +

    +

    + In My account you can select Default DMS query for your account. +

    + Custom queries +
    + +
    +

    5.8 Download

    +

    + To download or see a document just click its title. +

    +

    + Documents of display-able types such as PDF, PNG, JPG, ODT,... are displayed inline. If you want to download + them use the right mouse button's context menu of your browser and click on Save link as. +

    +

    + Multiple documents and folders can be downloaded at once. Select multiple items using check boxes on the left + side or using your mouse and <Shift> button. The open the conext menu using your mouse and click on + Download. Selected items will be zipped and offered for downloading. +

    + Download +
    + +
    +

    5.9 Email

    +

    + Single or multiple documents can be sent by email. Select one or multiple document and choose Email + from the context menu. +

    +

    + In the email form fill in email recipients and optionally a body text and click Send. +

    +

    + By default, selected documents are zipped and attached to the email. If you select links only, the + documents won't be directly attached but linked into the email's body instead. If you use + Public URLs check box, the documents' links will be made public and will be valid for the specified + period of time. +

    + Email +
    + +
    +

    5.10 Delete

    +

    + Select single or multiple items and from the context menu choose Delete. +

    +
    + +
    +

    5.11 Add documents

    +

    + Click on the green plus icon New document in the top right of the main view. Then you can use the + Browse button or simple drag and drop files into the drop-able area. Multiple files (max 10) can be selected. +

    +

    + Recently uploaded files ar listed on the screen. You can delete them, if they were + selected by mistake using the trash bin icon. When done, click the Uplad button. +

    +

    + If you use [+] icon, you can specify document's details and then you can directly use Upload and commit button. + In that case the following commit screeen will be skipped and uploaded documents will be directly stored. +

    + Upload +

    + Once the new files are uploaded, corresponding documents are created. Here you can specify additional attributes + of the new documents. E.g. version and custom fields if any. When done, click again on Upload button. + Then the new documents are committed and your are redirected to the parent folder with the new documents. +

    + Commit +
    + +
    +

    5.12 Update documents

    +

    + Attributes or content of any document can be altered in the Edit from. Use the context menu and click + Edit. Each change means a new revision. Click on the plus button New Revision. Then you can + update any of the document's attribute and you can also upoad a new content using New content field. +

    + Version +

    + The version of the document is automatically increased starting from 0.1. Major, minor and patch versions are + available. the rabge of each version varies from 0 to 999 and from A to Z. +

    +

    + When all required attributes are set, click Create button. +

    +
    + Note: +

    + The same effect you can achieve by uploading an updated document to its original location. A new + revision of the same document is created automatically after uploading. +

    +
    + New revision +
    + +
    +

    5.13 Documents links

    +

    + There is possible to create a link to another document or folder from the same or another folder of the same or + another project. Links behave as if operating directly on the target document or folder. It means for example + that if you download or email a link, the referenced file is sent to the user. +

    +

    + If the target document is going to be removed or moved the user will be warn about existing links and these + links will be automatically removed. +

    + Create a link of the current document or folder to another folder +

    + There is a command Link to in the context menu of a document or folder. After a click on that item a new + form New document link appears. The user is expected to choose a target project and folder as the + destination of the link just being created. +

    + Link to
    + Create a link to a document or folder in the current folder +

    + There is a command Link to in the context menu in the top right corner of the main view. After a click on + that item a new form New document link appears. The user is expected to choose a sorce project and folder + and optionaly document as the target of the link just being created. +

    + Link from
    + Internal +

    + Allow to create links to DMS objects. +

    + External +

    + Allow to create links to external objects using their URL. +

    + External link +
    + +
    +

    5.14 List of documents

    + Each line of the document list contains: +
      +
    • document's name
    • +
    • document's file
    • +
    • documnt's size, date of creation, version and author
    • +
    • document's context menu
    • +
    + DMS Main +

    + Using the context menu you can trigger the following commands: +

    +
      +
    • Edit
    • +
    • Copy/Move
    • +
    • Lock/Unlock
    • +
    • Assignment/Start/Approval
    • +
    • Download
    • +
    • Email
    • +
    • Edit content
    • +
    • Watch/Unwatch
    • +
    • Delete
    • +
    +
    + Note: +

    + List of available commands might vary according to the document's state and user's parmissions. +

    +
    +
    + +
    +

    5.15 Document's details

    +

    + Document's details are avaiable when you click on the Edit item in the context menu. A new form appears + then with a list of existing document's revisions. Each revision is represented by a box containg all attributes + related to the revision. +

    +

    + Except visible attributes, there is also a context menu in the top right corner of each revision's box containg + a command for displaying dowloading history - Download entries, downloading the particular revision - + Download and deleting the revision Delete. The Delete command is only available, if it is + not the last revision. +

    + Document's details +
    + +
    +

    5.16 Notifications

    +

    + Each project with active DMS module, each folder or document ca be set as "watched". Watch/Unwatch + command is available from context menus. Watched items are marked with a yellow star. +

    +

    + You will reveive email notifications when watched items are updated, deleted or downloaded. The notifications + about downloads must be explicitely switched on in your user's profile. +

    +

    + If a project or folder is marked as watched, notifications are sent for all folders and documents under the + given project or folder. +

    +
    + +
    +

    5.17 Approval workflow

    +

    + Thanks to approval workflows we can applied an approval process on each document. Approval workflows can be + defined in the project's settings or in the administration. +

    +

    + The approval workflow state is indicated by workflow status in the Workflow column. The initial state is + empty. If you move your mouse cursor over the status text and the approval workflow hasn't been finished yet, + you will see a list of users who are expected to do an approval. If the workflow is in the stat of + Waiting for an approval and the approval steps have assigned names, you will see the name of the current + approval step. +

    + Approval workflow +

    + You can check your open approval workflows on My Page Using the context menu you can approve documents + directly from there. If the command is missing, it means that you are not expected to do an approval in the + current approval step. +

    + My Page +
    + Approval workflow process +

    + The process itself is clearly described on the diagram below. +

    + Approval workflow's process +

    + And now step by step: +

    +

      +
    1. +

      + Assign an existing approval workflow to the selected document by clicking on Assignment. Then a + workflow assignment form appears: +

      + Assignment +

      + All project's and global approval workflows are selectable. To manage approval workflows, see + Approval Workflow of Project settings. Select a workflow and click on Submit button. + The selected workflow is assigned to the document. +

      +
    2. +
    3. + In the next step the assigned workflow must be started by on Start. +
    4. +
    5. +

      + When the workflow is started, all approvers in particular steps are expected to do an approval. The + document is locked to prevent all changes. +

      +

      + If you are one of the approvers of the current approval step, you can do an approval by clicking on + Approval. Then the approval form appears: +

      + Approval +

      + You have three options here, either approve, reject or delegate the current approval step. In case of + rejection or delegation you are obliged to comment it in the text field Your note.... Only + members of the project are offered for delegation. Your decision will be confirmed by clicking the + Submit button. +

      +
    6. +
    7. + If you has just approved the document and you are the last person of the approval chain, the document is + approved as a consequence of your approval. The status is changed to Approved. +
    8. +
    9. + If you has just rejected the document, the approval chain is finished immediately and the document is in + the state of Rejected. +
    10. +
    11. + If you approve it and you are not the last approver or you delegate your approval to someone else, the + workflow approval continues. +
    12. +
    +

    + Approved an rejected documents remain locked depending on the plugin’s settings. +

    + Log +

    + All workflow approvals are stored and are available in the log window. You can open the window by clicking the + workflow status text: +

    + Log +

    + Email notifications +

    +

    + Email notifications are sent according to the table as follows: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    EventReceiver
    The approval workflow is startedAll approvers of the first step
    An approval workflow step is finishedAll approvers of the next step and workflow owner
    The document has been approvedAll members of the project
    The document has been rejectedAll participants of the workflow and the workflow owner
    An approval step has been delegatedThe delegate
    Due date has been reachedAll approvers in the given step who haven't approved yet. These notifications are resent every working + day.
    +

    + Retrieving/Reassigning of an approval workflow +

    +

    + In case of a wrong approval workflow assigning or a need to restart the workflow, just add a new empty revision + from the document details. Then you can assign a new approval workflow to the document. The approval workflow is + always related to the last document revision. +

    +
    + +
    +

    5.18 Trash bin

    +

    + If documents or folders are deleted by a user, they are moved into the Trash bin only in fact and can be + restored again or definitively removed from the Trash bin. +

    +

    + The Trash bin is available from the main context menu. +

    +

    + The user interface of the trash bin is very similar to the documents view except available command. Only + commands for restoring and deleting are available. +

    +
    + Note: +

    + Once a document or folder is deleted from the trash bin, there is no way back! +

    +
    +

    + If you use Restore, the selected items are restored to their original position in DMS. +

    +
    + +
    +

    6 Activity module

    +

    + All changes related to a file revision and all downloads are recorded as activities. You can list any of them on + Activity tab. +

    + Activity +
    + +
    +

    7 WebDAV

    +

    + The document module of the project can be mounted as a web folder. In a mounted folder is available a complete + document and folder structure. When a user does an operation such as download/upload a document or + creating a new folder it is automatically recorded in DMS and it has the same effect as the operation would be + done in the web interface of DMS. +

    +

    + A URL to mount has the following format: +

    +

    + https://[your domain]/dmsf/webdav/[project identifier] +

    + WebDAV +
    + Note: +

    + You can mount mount all available documents using the following URL: +

    + https://[your domain]/dmsf/webdav +
    +

    Authentication

    +

    Basic

    +

    + If you switch the authentication method to Basic in the plugin's settings, users are authenticated by their login and password that are sent from a client to the server as a text. From that reason it is not considered as secure. +

    +

    Digest

    +

    + It's the default authentication method. Users are authenticated using a digest that is auto-generated after a successful login to Redmine or they can reset it in their user's profile (My account). To log in from a client, they again use their login and password. Login is case-sensitive. +

    +
    + +
    +

    8 REST API

    +

    + DMS exposes some of its data through a REST API. This API provides access and basic CRUD operations (create, + update, delete) for documents and folders. The API supports both XML and JSON formats. +

    +

    + Detailed description can be found here + togeather with a + sample shell scrit. +

    +
    + +
    +

    9 Wiki macros

    +

    + You can use DMS macros to link DMS documents and folders in Wiki formatted text. The wiki toolbar is extended + with a DMS button. The popup menu contains all available macros togeather with their description under the + Help command. +

    + Macros +
    + + + diff --git a/app/views/dmsf_help/en/wiki_syntax.html.erb b/app/views/dmsf_help/en/wiki_syntax.html.erb new file mode 100644 index 00000000..bed35d70 --- /dev/null +++ b/app/views/dmsf_help/en/wiki_syntax.html.erb @@ -0,0 +1,48 @@ + + + + +Wiki formatting + <%= stylesheet_link_tag 'wiki_syntax.css' %> + + + +

    DMS Syntax Reference

    + +

    DMS adds the following builtin macros:

    + +
    +
    Documents
    +
    +

    {{dmsf(17)}} - link to document #17

    +

    {{dmsf(17, File)}} - link to document #17 with the text "File"

    +

    {{dmsf(17, File, 10)}} - link to document #17 r10 with the text "File"

    +

    {{dmsfd(17)}} - link to the details of document #17

    +

    {{dmsfdesc(17)}} - link to the description of document #17

    +
    +
    Folders
    +
    +

    {{dmsff(5)}} - link to folder #5

    +

    {{dmsff(5, Folder)}} - link to folder #5 with the text "Folder"

    +
    +
    Images
    +
    +

    {{dmsf_image(8)}} - inline picture of document #8; the document must be an image file such as JPEG, PNG,...

    +

    {{dmsf_image(8, size=300)}}

    +

    {{dmsf_image(8, size=640x480)}}

    +

    {{dmsf_image(8, size=50%)}}

    +

    {{dmsf_image(8, height=300)}}

    +

    {{dmsf_image(8, width=300)}} - inline picture of document 8 with a custom size

    +

    {{dmsftn(8)}} - thumbnail of document #8

    +

    {{dmsftn(8, size=300)}} - thumbnail of document #8 with a custom size

    +
    +
    Approval Workflow
    +
    +

    {{dmsfw(8)}} - approval workflow status of document #8

    +
    +
    + +The document/folder's ID can be found in it's details. + + + \ No newline at end of file diff --git a/app/views/dmsf_links/_form.html.erb b/app/views/dmsf_links/_form.html.erb new file mode 100644 index 00000000..a9001146 --- /dev/null +++ b/app/views/dmsf_links/_form.html.erb @@ -0,0 +1,151 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

    <%= l(:title_create_link) %>

    + +<%= labelled_form_for @dmsf_link, remote: modal do |f| %> + <%= error_messages_for @dmsf_link %> + <%= f.hidden_field :project_id, value: @dmsf_link.project_id %> + <%= f.hidden_field :dmsf_folder_id, value: @dmsf_link.dmsf_folder_id if @dmsf_link.dmsf_folder_id %> + <%= f.hidden_field :type, value: @type %> + <%= f.hidden_field :dmsf_file_id, value: @dmsf_file_id %> + <%= f.hidden_field(:container, value: @container) if @container %> + <%= hidden_field_tag 'back_url', @back_url %> +
    + <% if (@type == 'link_from') && !@container %> +

    + <%= radio_button_tag :external_link, 'false', true %> <%= l(:label_internal) %>
    + <%= radio_button_tag :external_link, 'true', false %> <%= l(:label_external) %> +

    + <% end %> + + <% if (@type == 'link_from') && !@container %> + + <% end %> +

    + <%= f.text_field :name, required: true, max_length: 255 %> +

    +
    + <% if modal %> + <%= f.submit l(:button_create), class: 'button-positive', onclick: 'hideModal(this);' %> + <% else %> + <%= f.submit l(:button_create), class: 'button-positive' %> + <% end %> +
    +
    +<% end %> +<%= javascript_tag do %> +$(document).ready(function(){ + <%# Select2 extension, TODO: in case of a modal window, select2 makes problems %> + <% unless modal || @fast_links %> + $('#dmsf_link_target_project_id').select2(); + $('#dmsf_link_target_folder_id').select2(); + $('#dmsf_link_target_file_id').select2(); + <% end %> + <%# Suggest a link's name when a file is selected %> + $('#dmsf_link_target_file_id').change(function () { + var linkName = $('#dmsf_link_name'); + var name = linkName.val(); + var dirName = $('#dmsf_link_target_folder_id option:selected').text().replace(/\./g, '') + var fileName = $('#dmsf_link_target_file_id option:selected').text().replace(/\./g, '') + if((name == '') || name == dirName) { + linkName.val(fileName); + } + }); + <%# Suggest a link's name when a folder is selected %> + $('#dmsf_link_target_folder_id').change(function () { + var linkName = $('#dmsf_link_name'); + var name = linkName.val(); + var dirName = $('#dmsf_link_target_folder_id option:selected').text().replace(/\./g, '') + if(name == '') { + linkName.val(dirName); + } + }); + <%# Internal/External link switch %> + $("input[name=external_link]:radio").change(function(){ + $("#dmsf_link_internal").toggle(); + $("#dmsf_link_external").toggle(); + $("#dmsf_link_external_url").toggleClass('required', $(this).val()); + var labelUrl = $('label[for="dmsf_link_external_url"]'); + labelUrl.toggleClass('required', $(this).val()); + if(labelUrl.find(".required").length == 0){ + labelUrl.append(' *'); + } + }); +}); +<% end %> diff --git a/app/views/dmsf_links/autocomplete_for_folder.js.erb b/app/views/dmsf_links/autocomplete_for_folder.js.erb new file mode 100644 index 00000000..0de36010 --- /dev/null +++ b/app/views/dmsf_links/autocomplete_for_folder.js.erb @@ -0,0 +1,22 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +$('#dmsf_link_target_file_id').empty().append('<%= escape_javascript( + options_for_select(DmsfFolder.file_list(files_for_select(params[:dmsf_link][:target_project_id], + params[:dmsf_link][:target_folder_id])))) %>'); diff --git a/app/views/dmsf_links/autocomplete_for_project.js.erb b/app/views/dmsf_links/autocomplete_for_project.js.erb new file mode 100644 index 00000000..ffd7e067 --- /dev/null +++ b/app/views/dmsf_links/autocomplete_for_project.js.erb @@ -0,0 +1,24 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +var link = $('#dmsf_link_target_folder_id'); + +link.empty().append('<%= escape_javascript( + folder_tree_options_for_select(DmsfFolder.directory_tree(params[:dmsf_link][:target_project_id]))) %>'); +link.change(); diff --git a/app/views/dmsf_links/create.api.rsb b/app/views/dmsf_links/create.api.rsb new file mode 100644 index 00000000..5aa3aceb --- /dev/null +++ b/app/views/dmsf_links/create.api.rsb @@ -0,0 +1,4 @@ +api.dmsf_link do + api.id @dmsf_link.id + api.title @dmsf_link.title +end \ No newline at end of file diff --git a/app/views/dmsf_links/create.js.erb b/app/views/dmsf_links/create.js.erb new file mode 100644 index 00000000..0fb92259 --- /dev/null +++ b/app/views/dmsf_links/create.js.erb @@ -0,0 +1,35 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +var linksSpan = $("#dmsf_links_attachments_fields"); +var linkId = "<%= @dmsf_link.id %>"; +var linkName = "<%= @dmsf_link.name %>"; +var title = "<%= l(:label_dmsf_wokflow_action_assign) %>"; +var project = "<%= @project.identifier %>" +var awf = false; + +<% file = @dmsf_link.target_file %> +<% if file && !file.locked? && User.current.allowed_to?(:file_approval, file.project) %> + <% revision = file.last_revision %> + <% if revision&.workflow.nil? %> + awf = true; + <% end %> +<% end %> + +dmsfAddLink(linksSpan, linkId, linkName, title, project, awf); diff --git a/app/views/dmsf_links/new.html.erb b/app/views/dmsf_links/new.html.erb new file mode 100644 index 00000000..93964b24 --- /dev/null +++ b/app/views/dmsf_links/new.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= render partial: 'form', locals: { modal: false } %> \ No newline at end of file diff --git a/app/views/dmsf_links/new.js.erb b/app/views/dmsf_links/new.js.erb new file mode 100644 index 00000000..4a146930 --- /dev/null +++ b/app/views/dmsf_links/new.js.erb @@ -0,0 +1,21 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +$('#ajax-modal').html('<%= escape_javascript(render partial: 'form', locals: { modal: true }) %>'); +showModal('ajax-modal', '40%'); \ No newline at end of file diff --git a/app/views/dmsf_mailer/files_deleted.html.erb b/app/views/dmsf_mailer/files_deleted.html.erb new file mode 100644 index 00000000..08bc25a5 --- /dev/null +++ b/app/views/dmsf_mailer/files_deleted.html.erb @@ -0,0 +1,29 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= link_to @author, user_url(@author) %> <%= l(:text_email_doc_deleted) %> +<%= link_to @project, project_url(@project) %> <%= l(:text_email_doc_follows) %> +<% @files.each do |file| %> +

    + <%= h(file.dmsf_path_str) %> (<%= file.name %>) + <% if file.last_revision %> + , <%= number_to_human_size(file.last_revision.size) %>, <%= l(:label_dmsf_version) %> <%= file.last_revision.version %> + <% end %> +

    +<% end %> \ No newline at end of file diff --git a/app/views/dmsf_mailer/files_deleted.text.erb b/app/views/dmsf_mailer/files_deleted.text.erb new file mode 100644 index 00000000..5cc280c9 --- /dev/null +++ b/app/views/dmsf_mailer/files_deleted.text.erb @@ -0,0 +1,26 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= @author.name %> <%= l(:text_email_doc_deleted) %> <%= @project.name %> <%= l(:text_email_doc_follows) %> +<% @files.each do |file| %> + <%= h(file.dmsf_path_str) %> (<%= file.name %>) + <% if file.last_revision %> + , <%= number_to_human_size(file.last_revision.size) %>, <%= l(:label_dmsf_version) %> <%= file.last_revision.version %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/dmsf_mailer/files_downloaded.html.erb b/app/views/dmsf_mailer/files_downloaded.html.erb new file mode 100644 index 00000000..c7b2b525 --- /dev/null +++ b/app/views/dmsf_mailer/files_downloaded.html.erb @@ -0,0 +1,31 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= link_to @author, user_url(@author) %> <%= l(:text_email_doc_downloaded) %> +<%= link_to @project, project_url(@project) %> <%= l(:text_email_doc_follows) %> +(<%= Time.now %>, <%= @remote_ip %>) +<% @files.each do |file| %> +

    + <%= h(file.dmsf_path_str) %> (<%= file.name %>) + <% if file.last_revision %> + , <%= number_to_human_size(file.last_revision.size) %>, + <%= l(:label_dmsf_version) %> <%= file.last_revision.version %> + <% end %> +

    +<% end %> diff --git a/app/views/dmsf_mailer/files_downloaded.text.erb b/app/views/dmsf_mailer/files_downloaded.text.erb new file mode 100644 index 00000000..2377bbe2 --- /dev/null +++ b/app/views/dmsf_mailer/files_downloaded.text.erb @@ -0,0 +1,28 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= @author.name %> <%= l(:text_email_doc_downloaded) %> <%= @project.name %> <%= l(:text_email_doc_follows) %> +(<%= Time.now %>, <%= @remote_ip %>) +<% @files.each do |file| %> + <%= h(file.dmsf_path_str) %> (<%= file.name %>) + <% if file.last_revision %> + , <%= number_to_human_size(file.last_revision.size) %>, + <%= l(:label_dmsf_version) %> <%= file.last_revision.version %> + <% end %> +<% end %> diff --git a/app/views/dmsf_mailer/files_updated.html.erb b/app/views/dmsf_mailer/files_updated.html.erb new file mode 100644 index 00000000..c8a03843 --- /dev/null +++ b/app/views/dmsf_mailer/files_updated.html.erb @@ -0,0 +1,34 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= link_to @author, user_url(@author) %> <%= l(:text_email_doc_updated) %> +<%= link_to @project, project_url(@project) %> <%= l(:text_email_doc_follows) %> +<% @files.each do |file| %> +

    + <%= link_to h(file.dmsf_path_str), dmsf_file_url(file, download: '') %> + (<%= file.name %>), + <%= number_to_human_size(file.last_revision.size) %>, + <%= l(:label_dmsf_version) %> <%= file.last_revision.version %>, + <%= "#{file.last_revision.workflow_str(true)}," if file.last_revision.workflow_str(true) != l(:title_none) %> + <%= link_to l(:link_details, title: h(file.title)), dmsf_file_url(file) %> + <% if file.last_revision.comment.present? %> +
        <%= h(file.last_revision.comment) %> + <% end %> +

    +<% end %> \ No newline at end of file diff --git a/app/views/dmsf_mailer/files_updated.text.erb b/app/views/dmsf_mailer/files_updated.text.erb new file mode 100644 index 00000000..0b55fb2d --- /dev/null +++ b/app/views/dmsf_mailer/files_updated.text.erb @@ -0,0 +1,31 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= @author.name %> <%= l(:text_email_doc_updated) %> +<%= @project.name %> <%= l(:text_email_doc_follows) %> +<% @files.each do |file| %> + <%= h(file.dmsf_path_str) %> (<%= file.name %>), + <%= number_to_human_size(file.last_revision.size) %>, + <%= l(:label_dmsf_version) %> <%= file.last_revision.version %>, + <%= "#{file.last_revision.workflow_str(true)}," if file.last_revision.workflow_str(true) != l(:title_none) %> + <%= dmsf_file_url(file) %> + <% if file.last_revision.comment.present? %> + <%= l(:label_comment) %> <%= h(file.last_revision.comment) %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/dmsf_mailer/send_documents.html.erb b/app/views/dmsf_mailer/send_documents.html.erb new file mode 100644 index 00000000..8819d25b --- /dev/null +++ b/app/views/dmsf_mailer/send_documents.html.erb @@ -0,0 +1,82 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= textilizable(@body) %> + +<% if @links_only %> + <% folders = [] %> + <% files = [] %> + <% if @folders.present? %> + <% JSON.parse(@folders).each do |id| %> + <% folder = DmsfFolder.find_by(id: id) %> + <% if folder %> + <% folder.folder_tree.each do |name, i| %> + <% dir = DmsfFolder.find_by(id: i) %> + <% if dir && !folders.include?(dir) %> +
    + <%= link_to h(dir.dmsf_path_str), dmsf_folder_url(id: dir.project_id, folder_id: dir.id, only_path: false) %> +

    + <% dir.dmsf_files.each do |file| %> + <% unless files.include?(file) %> + <% if @public_urls %> + <% dmsf_public_url = DmsfPublicUrl.new %> + <% dmsf_public_url.dmsf_file = file %> + <% dmsf_public_url.user = @author %> + <% dmsf_public_url.expire_at = @expired_at %> + <% dmsf_public_url.save %> + <%= link_to h(file.title), dmsf_public_urls_url(token: dmsf_public_url.token) %> + (<%= link_to h(file.name), dmsf_public_urls_url(token: dmsf_public_url.token) %>) + <% else %> + <%= link_to(h(file.title), dmsf_file_url(file)) %> +  (<%= link_to(h(file.name), dmsf_file_url(file)) %>) + <% end %> +
    + <% files << file %> + <% end %> + <% end %> + <% folders << dir %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% if @files.present? %> +
    + <% JSON.parse(@files).each do |id| %> + <% file = DmsfFile.find_by_id id %> + <% if file && !files.include?(file) %> + <% if @public_urls %> + <% dmsf_public_url = DmsfPublicUrl.new %> + <% dmsf_public_url.dmsf_file = file %> + <% dmsf_public_url.user = @author %> + <% dmsf_public_url.expire_at = @expired_at %> + <% dmsf_public_url.save %> + <%= link_to h(file.title), dmsf_public_urls_url(token: dmsf_public_url.token) %> +  (<%= link_to h(file.name), dmsf_public_urls_url(token: dmsf_public_url.token) %>) + <% else %> + <%= link_to h(file.title), dmsf_file_url(file) %> +  (<%= link_to h(file.name), dmsf_file_url(file) %>) + <% end %> +
    + <% files << file %> + <% end %> + <% end %> + <% end %> +<% end %> + \ No newline at end of file diff --git a/app/views/dmsf_mailer/send_documents.text.erb b/app/views/dmsf_mailer/send_documents.text.erb new file mode 100644 index 00000000..4157c7c7 --- /dev/null +++ b/app/views/dmsf_mailer/send_documents.text.erb @@ -0,0 +1,71 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= @body %> + +<% if @links_only %> + <% folders = [] %> + <% files = [] %> + <% if @folders.present? %> + <% JSON.parse(@folders).each do |id| %> + <% folder = DmsfFolder.find_by(id: id) %> + <% if folder && !folders.include?(folder) %> + <% folder.folder_tree.each do |name, i| %> + <% dir = DmsfFolder.find_by(id: i) %> + <% if dir %> + <%= dir.dmsf_path_str %> + <% dir.dmsf_files.each do |file| %> + <% unless files.include?(file) %> + <% if @public_urls %> + <% dmsf_public_url = DmsfPublicUrl.new %> + <% dmsf_public_url.dmsf_file = file %> + <% dmsf_public_url.user = @author %> + <% dmsf_public_url.expire_at = @expired_at %> + <% dmsf_public_url.save %> + <%= dmsf_public_urls_url(token: dmsf_public_url.token) %> + <% else %> + <%= dmsf_file_url(file) %> + <% end %> + <% files << file %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% if @files.present? %> + <% JSON.parse(@files).each do |id| %> + <% file = DmsfFile.find_by_id id %> + <% if file && !files.include?(file) %> + <% if @public_urls %> + <% dmsf_public_url = DmsfPublicUrl.new %> + <% dmsf_public_url.dmsf_file = file %> + <% dmsf_public_url.user = @author %> + <% dmsf_public_url.expire_at = @expired_at %> + <% dmsf_public_url.save %> + <%= dmsf_public_urls_url(token: dmsf_public_url.token) %> + <% else %> + <%= dmsf_file_url(file) %> + <% end %> + <% files << file %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/dmsf_mailer/workflow_notification.html.erb b/app/views/dmsf_mailer/workflow_notification.html.erb new file mode 100644 index 00000000..8e131109 --- /dev/null +++ b/app/views/dmsf_mailer/workflow_notification.html.erb @@ -0,0 +1,32 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

    <%= @user.name %>,

    +

    + <%= @text1 %> +

    +

    + <%= @text2 %> + <% if @revision.dmsf_file.dmsf_folder %> + <%= link_to @revision.dmsf_file.dmsf_folder.title, + dmsf_folder_url(id: @revision.dmsf_file.project, folder_id: @revision.dmsf_file.dmsf_folder) %> + <% else %> + <%= link_to l(:link_documents), dmsf_folder_url(id: @revision.dmsf_file.project) %> + <% end %>. +

    diff --git a/app/views/dmsf_mailer/workflow_notification.text.erb b/app/views/dmsf_mailer/workflow_notification.text.erb new file mode 100644 index 00000000..840c85b7 --- /dev/null +++ b/app/views/dmsf_mailer/workflow_notification.text.erb @@ -0,0 +1,26 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= @user.name %>, +<%= @text1 %> +<% if @revision.dmsf_file.dmsf_folder %> +<%= @text2 %> <%= dmsf_folder_url(id: @revision.dmsf_file.project, folder_id: @revision.dmsf_file.dmsf_folder) %>. +<% else %> +<%= @text2 %> <%= dmsf_folder_url(id: @revision.dmsf_file.project) %>. +<% end %> \ No newline at end of file diff --git a/app/views/dmsf_public_urls/_new.html.erb b/app/views/dmsf_public_urls/_new.html.erb new file mode 100644 index 00000000..2d0e95b1 --- /dev/null +++ b/app/views/dmsf_public_urls/_new.html.erb @@ -0,0 +1,26 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # + # Karel Pičman + # + # 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 + # . +%> + +<% classes = 'hol' unless RedmineDmsf.dmsf_documents_email_links_only? %> + + <%= check_box_tag 'email[public_urls]', 1, false %> <%= l(:label_public_urls) %> + <%= date_field_tag('email[expired_at]', '', value: (DateTime.current + 3.days).to_date, size: 10) + + calendar_for('email_expired_at') %> + diff --git a/app/views/dmsf_state/_user_pref.html.erb b/app/views/dmsf_state/_user_pref.html.erb new file mode 100644 index 00000000..81b54215 --- /dev/null +++ b/app/views/dmsf_state/_user_pref.html.erb @@ -0,0 +1,76 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% member = Member.find_by(project_id: @project.id, user_id: User.current.id) %> +<%= form_tag(dmsf_user_pref_save_path(@project)) do %> + <% if member %> +
    + <%= l(:link_user_preferences) %> + <% if Setting.notified_events.include?('dmsf_legacy_notifications') %> +

    + <%= content_tag :label, l(:label_notifications) %> + <%= select_tag 'email_notify', + options_for_select([[l(:select_option_default), nil], + [l(:select_option_activated), true], [l(:select_option_deactivated), false]], + selected: member.dmsf_mail_notification) %> +

    + <% end %> +

    + <%= content_tag :label, l(:label_title_format) %> + <%= text_field_tag 'title_format', member.dmsf_title_format, size: 10 %> + <%= l(:text_title_format) %> +

    +

    + <%= content_tag :label, l(:label_dmsf_fast_links) %> + <%= check_box_tag 'fast_links', 1, member.dmsf_fast_links %> + + <%= l(:text_dmsf_fast_links_info) %> + +

    +
    + <% end %> + +
    + <%= l(:field_project) %> <%= l(:label_preferences) %> + <% if RedmineDmsf.dmsf_act_as_attachable? %> +

    + <%= content_tag(:label, "#{l(:label_act_as_attachable)}:") %> + <%= select_tag 'act_as_attachable', + options_for_select([ + [l(:label_attachment_plural) + ' & ' + l(:menu_dmsf), Project::ATTACHABLE_DMS_AND_ATTACHMENTS], + [l(:label_attachment_plural), Project::ATTACHABLE_ATTACHMENTS], + ], selected: @project.dmsf_act_as_attachable) %> +

    + <% end %> +

    + <%= content_tag(:label, "#{l(:label_default_query)}:") %> + <% options = [[l(:label_none), nil]] %> + <% options.concat DmsfQuery.only_public.where(project_id: nil).pluck(:name, :id) %> + <%= select_tag 'default_dmsf_query', + options_for_select(options, selected: @project.default_dmsf_query_id) %> + <%= l('text_allowed_queries_to_select') %> +

    + <%= call_hook(:view_dmsf_state_user_pref, { project: @project }) %> +
    + +
    + <%= submit_tag l(:submit_save), title: l(:title_save_preferences), class: 'button-positive' %> +
    + +<% end %> diff --git a/app/views/dmsf_upload/_form.html.erb b/app/views/dmsf_upload/_form.html.erb new file mode 100644 index 00000000..60eb4de5 --- /dev/null +++ b/app/views/dmsf_upload/_form.html.erb @@ -0,0 +1,135 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + + + + <% if defined?(container) && container && container.saved_dmsf_attachments.present? %> + <% container.saved_dmsf_attachments.each_with_index do |attachment, i| %> + <% i += 1 %> + + <%= hidden_field_tag "dmsf_attachments[p#{i}][token]", "#{attachment.token}" %> + <%= sprite_icon('attachment', icon_only: true, size: 16, css_class: 'svg-attachment') %> + <%= text_field_tag("dmsf_attachments[p#{i}][filename]", attachment.filename, + class: 'filename icon icon-attachment readonly') %> + <%= link_to sprite_icon('del', l(:button_delete), icon_only: true), + dmsf_attachment_path(attachment, attachment_id: "p#{i}", format: 'js'), + method: 'delete', remote: true, title: l(:button_delete), + class: 'remove-upload icon-only icon-del' %> + <% wf = container.saved_dmsf_attachments_wfs[attachment.id] %> + <% if wf %> + <%= link_to sprite_icon('checked', l(:title_assigned), icon_only: true), '#', remote: true, + title: l(:title_assigned), class: 'modify-upload icon-only icon-ok' %> + <%= hidden_field_tag("dmsf_attachments_wfs[p#{i}]", wf.id) %> + <% else %> + <%= link_to sprite_icon('checked', l(:title_assignment), icon_only: true), + assign_dmsf_workflow_path(id: i, project_id: container.project&.id, + attachment_id: i), title: l(:label_dmsf_wokflow_action_assign), + remote: true, class: 'modify-upload icon-only icon-ok' %> + <% end %> + + <% end %> + <% end %> + + + + <% if defined?(container) && container && container.saved_dmsf_links.present? %> + <% container.saved_dmsf_links.each_with_index do |dmsf_link, index| %> + + + + <%= link_to '', dmsf_link_attachment_path(dmsf_link, link_id: "#{index}", format: 'js'), + method: 'delete', remote: true, class: 'remove-upload icon-only icon-del' %> + <% wf = container.saved_dmsf_links_wfs[dmsf_link.id] %> + <% if wf %> + <%= link_to sprite_icon('checked', l(:title_assigned), icon_only: true), '#', remote: true, + class: 'modify-upload icon-only icon-ok' %> + <%= hidden_field_tag("dmsf_links_wfs[#{dmsf_link.id}]", wf.id) %> + <% else %> + <%= render partial: 'dmsf_workflows/approval_workflow_button', + locals: { file: dmsf_link.target_file, + file_approval_allowed: User.current.allowed_to?(:file_approval, dmsf_link.target_file.project), + workflows_available: DmsfWorkflow.where( + ['project_id = ? OR project_id IS NULL', dmsf_link.target_file.project_id]).exists?, + project: dmsf_link.target_file.project, wf: wf, dmsf_link_id: dmsf_link.id } %> + <% end %> + + <% end %> + <% end %> + + + + <% + if defined?(container) && container + project = container.project + folder = container.system_folder + else + project = @project + folder = @folder + end + project_or_folder = folder ? folder : project + files = [] + if project_or_folder + project_or_folder.dmsf_files.visible.each do |dmsf_file| + rev = dmsf_file.last_revision + if rev + files << [dmsf_file.name, rev.major_version, rev.minor_version, rev.patch_version, dmsf_file.locked_for_user? ] + end + end + end + %> + <%= file_field_tag 'dmsf_attachments[dummy][file]', + id: nil, + class: 'file_selector', + multiple: multiple, + onchange: 'dmsfAddInputFiles(this);', + data: { + max_number_of_files_message: l(:error_attachments_too_many, + max_number_of_files: (multiple ? 10 : 1)), + max_file_size: Setting.attachment_max_size.to_i.kilobytes, + max_file_size_message: l(:error_attachment_too_big, + max_size: number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)), + max_concurrent_uploads: Redmine::Configuration['max_concurrent_ajax_uploads'].to_i, + upload_path: dmsf_uploads_path(format: 'js'), + project: project&.identifier, + awf: awf, + dmsf_file_details_form: controller.send(:render_to_string, + { partial: 'dmsf_upload/upload_file', + locals: { upload: DmsfUpload.new(project, folder, nil), i: 0 } }), + dmsf_file_details_form_locked: controller.send(:render_to_string, + { partial: 'dmsf_upload/upload_file_locked', + locals: { upload: DmsfUpload.new(project, folder, nil), i: 0 } }), + files: JSON.generate(files) + } + %> + +<% if defined?(container) && container %> + + <%= link_to sprite_icon('add', l(:label_link_from)), + new_dmsf_link_path(project_id: project&.id, type: 'link_from', container: container.class.name), + title: l(:title_create_link), class: 'icon icon-add file_selector', remote: true %> + +<% end %> + + (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) + diff --git a/app/views/dmsf_upload/_upload_file.html.erb b/app/views/dmsf_upload/_upload_file.html.erb new file mode 100644 index 00000000..4ed818e9 --- /dev/null +++ b/app/views/dmsf_upload/_upload_file.html.erb @@ -0,0 +1,79 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +
    + <%= 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 %> +
    +
    +

    + <%= label_tag "committed_files[#{i}][title]", l(:label_title) %> + <%= text_field_tag "committed_files[#{i}][title]", upload.title, required: true %> +

    +
    +
    +

    + <%= label_tag "committed_files[#{i}][name]", l(:label_filename) %> + <%= text_field_tag "committed_files[#{i}][name]", upload.name, readonly: true %> +

    +
    +
    +

    + <%= label_tag "committed_files[#{i}][description]", l(:label_description) %> + <%= text_area_tag "committed_files[#{i}][description]", upload.description, rows: 6, + class: 'wiki-edit dmsf-description' %> +

    +
    +
    +

    + <%= render partial: 'dmsf_files/version_selector', locals: { + label_tag_name: "committed_files[#{i}][version]", + select_tag_name_patch: "committed_files[#{i}][version_patch]", + select_tag_name_minor: "committed_files[#{i}][version_minor]", + select_tag_name_major: "committed_files[#{i}][version_major]", + revision_or_upload: upload } %> +

    +

    + <%= label_tag "committed_files[#{i}][mime_type]", l(:label_mime) %> + <%= text_field_tag "committed_files[#{i}][mime_type]", h(upload.mime_type), readonly: true %> +

    +

    + <%= label_tag "committed_files[#{i}][human_size]", l(:label_size) %> + <%= text_field_tag "committed_files[#{i}][human_size]", number_to_human_size(upload.size), readonly: true %> +

    +
    +
    +
    + <% upload.custom_values.each do |value| %> + <% value.value = nil if value.custom_field.dmsf_not_inheritable %> +

    <%= custom_field_tag_with_label "committed_files[#{i}]", value %>

    + <% end %> +
    +
    +
    +

    + <%= label_tag "committed_files[#{i}][comment]", l(:label_comment) %> + <%= text_area_tag "committed_files[#{i}][comment]", upload.comment, rows: 2, class: 'wiki-edit dmsf-description' %> +

    +
    + +<%= wikitoolbar_for "committed_files_#{i}_description" %> +<%= wikitoolbar_for "committed_files_#{i}_comment" %> diff --git a/app/views/dmsf_upload/_upload_file_locked.html.erb b/app/views/dmsf_upload/_upload_file_locked.html.erb new file mode 100644 index 00000000..c2956516 --- /dev/null +++ b/app/views/dmsf_upload/_upload_file_locked.html.erb @@ -0,0 +1,65 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +
    +

    <%= l(:info_file_locked) %>

    +
    +
    +

    + <%= label_tag "committed_files[#{i}][title]", l(:label_title) %> + <%= text_field_tag "committed_files[#{i}][title]", upload.title, readonly: true %> +

    +
    +
    +

    + <%= label_tag "committed_files[#{i}][name]", l(:label_filename) %> + <%= text_field_tag "committed_files[#{i}][name]", upload.name, readonly: true %> +

    +
    +
    +
    +
    +

    + <%= label_tag "committed_files[#{i}][version]", l(:label_dmsf_version) %> + <%= text_field_tag "committed_files[#{i}][version]", + "#{DmsfUploadHelper::gui_version(upload.major_version)}.#{DmsfUploadHelper::gui_version(upload.minor_version)}", + readonly: true %> +

    +

    + <%= label_tag "committed_files[#{i}][mime_type]", l(:label_mime) %> + <%= text_field_tag "committed_files[#{i}][mime_type]", upload.mime_type, readonly: true %> +

    +

    + <%= label_tag "committed_files[#{i}][human_size]", l(:label_size) %> + <%= text_field_tag "committed_files[#{i}][human_size]", number_to_human_size(upload.size), readonly: true %> +

    +
    +
    +
    + <% upload.custom_values.each do |value| %> +

    + <%= label_tag "committed_files[#{i}][#{value.custom_field.id}]", value.custom_field.name %> + <% value.value = nil if value.custom_field.dmsf_not_inheritable %> + <%= text_field_tag "committed_files[#{i}][#{value.custom_field.id}]", value.value, readonly: true %> +

    + <% end %> +
    +
    +
    +
    diff --git a/app/views/dmsf_upload/commit.api.rsb b/app/views/dmsf_upload/commit.api.rsb new file mode 100644 index 00000000..3cac3721 --- /dev/null +++ b/app/views/dmsf_upload/commit.api.rsb @@ -0,0 +1,8 @@ +api.array :dmsf_files, api_meta(total_count: @files.size) do + @files.each do |file| + api.file do + api.id file.id + api.name file.name + end + end +end \ No newline at end of file diff --git a/app/views/dmsf_upload/delete_dmsf_attachment.js.erb b/app/views/dmsf_upload/delete_dmsf_attachment.js.erb new file mode 100644 index 00000000..959cea40 --- /dev/null +++ b/app/views/dmsf_upload/delete_dmsf_attachment.js.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 +# . +%> + +$('#dmsf_attachments_<%= j params[:attachment_id] %>').remove(); diff --git a/app/views/dmsf_upload/delete_dmsf_link_attachment.js.erb b/app/views/dmsf_upload/delete_dmsf_link_attachment.js.erb new file mode 100644 index 00000000..6073e464 --- /dev/null +++ b/app/views/dmsf_upload/delete_dmsf_link_attachment.js.erb @@ -0,0 +1,21 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . + # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +%> + +$('#dmsf_links_attachments_<%= j params[:link_id] %>').remove(); diff --git a/app/views/dmsf_upload/multi_upload.html.erb b/app/views/dmsf_upload/multi_upload.html.erb new file mode 100644 index 00000000..3161e6c7 --- /dev/null +++ b/app/views/dmsf_upload/multi_upload.html.erb @@ -0,0 +1,39 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% heads_for_wiki_formatter %> + +<%= render partial: '/dmsf/path', + locals: { folder: @folder, filename: nil, title: l(:label_attachment_new) } %> + +<%= render partial: 'dmsf/description' %> + +<%= form_tag({ controller: 'dmsf_upload', action: 'upload_files', id: @project, folder_id: @folder }, + { id: 'uploadform', multipart: true }) do %> + + <%= render partial: 'dmsf_upload/form', + locals: { multiple: true, container: nil, description: true, awf: false } %> + +
    + <%= submit_tag l(:label_upload), data: { cy: 'button__submit--dmsf-upload--project' }, 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' %> +
    +<% end %> diff --git a/app/views/dmsf_upload/upload.api.rsb b/app/views/dmsf_upload/upload.api.rsb new file mode 100644 index 00000000..0a399ec1 --- /dev/null +++ b/app/views/dmsf_upload/upload.api.rsb @@ -0,0 +1,3 @@ +api.upload do + api.token @attachment.token +end diff --git a/app/views/dmsf_upload/upload.js.erb b/app/views/dmsf_upload/upload.js.erb new file mode 100644 index 00000000..9c89ed70 --- /dev/null +++ b/app/views/dmsf_upload/upload.js.erb @@ -0,0 +1,33 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +var fileSpan = $('#dmsf_attachments_<%= j params[:attachment_id] %>'); +<% if @attachment.new_record? %> + fileSpan.hide(); + alert("<%= escape_javascript @attachment.errors.full_messages.to_sentence %>"); +<% else %> +$('', { type: 'hidden', name: 'dmsf_attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan); +fileSpan.find('a.dmsf-remove-upload') + .attr({ + "data-remote": true, + "data-method": 'delete', + href: '<%= j dmsf_attachment_path(@attachment, attachment_id: params[:attachment_id], format: 'js') %>' + }) + .off('click'); +<% end %> diff --git a/app/views/dmsf_upload/upload_file.html.erb b/app/views/dmsf_upload/upload_file.html.erb new file mode 100644 index 00000000..54673ff2 --- /dev/null +++ b/app/views/dmsf_upload/upload_file.html.erb @@ -0,0 +1,27 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +{ + "original_filename":"<%= @tempfile.original_filename.html_safe %>", + "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 %>" +} diff --git a/app/views/dmsf_upload/upload_files.html.erb b/app/views/dmsf_upload/upload_files.html.erb new file mode 100644 index 00000000..ac6d6cde --- /dev/null +++ b/app/views/dmsf_upload/upload_files.html.erb @@ -0,0 +1,68 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # # 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 + # . +%> + +<% html_title l(:dmsf) %> + +<%= render partial: '/dmsf/path', + locals: { folder: @folder, filename: nil, title: l(:heading_uploaded_files) } %> + +<%= render partial: 'dmsf/description' %> + +<% if @uploads.size > 1 %> + +<% end %> + +<% unless @uploads.empty? %> + <%= form_tag({ action: 'commit_files', id: @project, folder_id: @folder }, method: :post) do %> + <% @uploads.each_with_index do |upload, i| %> + <% if upload.locked %> + <%= render partial: 'upload_file_locked', locals: { upload: upload, i: i } %> + <% else %> + <%= render partial: 'upload_file', locals: { upload: upload, i: i } %> + <% end %> + <% end %> +
    + <%= submit_tag l(:label_dmsf_commit), + data: { cy: 'button__submit__commit-file--project' }, + class: 'button-positive', + onclick: "$('#ajax-indicator').show();" %> +
    + <% end %> +<% end %> + +<%= javascript_tag do %> + // When the user scrolls the page, execute scrollFunction + window.onscroll = function() { + scrollFunction(); + }; + let scroller = $(".dmsf-scroll"); + let firstUploadBox = $(".dmfs-box-tabular:first"); + let offset = firstUploadBox.offset(); + // Add the sticky class to the header when you reach its scroll position. + // Remove "sticky" when you leave the scroll position + function scrollFunction() { + if (window.pageYOffset > offset.top) { + scroller.addClass("dmsf-sticky"); + } else { + scroller.removeClass("dmsf-sticky"); + } + } +<% end %> diff --git a/app/views/dmsf_workflows/_action.html.erb b/app/views/dmsf_workflows/_action.html.erb new file mode 100644 index 00000000..dfadb347 --- /dev/null +++ b/app/views/dmsf_workflows/_action.html.erb @@ -0,0 +1,55 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

    <%= l(:field_label_dmsf_workflow) %>

    + +<%= form_tag({ controller: 'dmsf_workflows', action: 'new_action'}, method: :post, id: 'new-action-form') do %> + <%= hidden_field_tag :dmsf_workflow_step_assignment_id, params[:dmsf_workflow_step_assignment_id] %> + <%= hidden_field_tag :dmsf_file_revision_id, params[:dmsf_file_revision_id] %> + <%= hidden_field_tag :back_url, params[:back_url] %> +

    + +

    +

    + +
    + <%= text_area_tag :note, '', placeholder: l(:message_dmsf_wokflow_note), style: 'width: 90%' %> +

    + +
    + <%= label_tag 'delegate', l(:label_dmsf_wokflow_action_delegate) %>
    + <%= text_field_tag 'user_search', nil %> + <%= javascript_tag "observeSearchfield('user_search', null, '#{escape_javascript autocomplete_for_user_dmsf_workflow_path(@dmsf_workflow, dmsf_workflow_step_assignment_id: params[:dmsf_workflow_step_assignment_id], dmsf_file_revision_id: params[:dmsf_file_revision_id])}')" %> +
    + <%= render_principals_for_new_dmsf_workflow_users( + @dmsf_workflow, params[:dmsf_workflow_step_assignment_id], params[:dmsf_file_revision_id]) %> +
    +
    + +

    + <%= submit_tag l(:button_submit), name: 'commit' %> + <%= submit_tag l(:button_cancel), name: 'cancel', onclick: 'hideModal(this);' %> +

    +<% end %> diff --git a/app/views/dmsf_workflows/_approval_workflow_button.html.erb b/app/views/dmsf_workflows/_approval_workflow_button.html.erb new file mode 100644 index 00000000..a0db169f --- /dev/null +++ b/app/views/dmsf_workflows/_approval_workflow_button.html.erb @@ -0,0 +1,71 @@ +<% + # + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% if file_approval_allowed %> + <% case file.last_revision.workflow %> + <% when DmsfWorkflow::STATE_WAITING_FOR_APPROVAL %> + <% if wf %> + <% assignments = wf.next_assignments(file.last_revision.id) %> + <% index = assignments.find_index{|assignment| assignment.user_id == User.current.id} if assignments %> + <% if assignments && index %> + <%= link_to sprite_icon('checked', ''), + action_dmsf_workflow_path(project_id: project.id, id: wf.id, + dmsf_workflow_step_assignment_id: assignments[index].id, + dmsf_file_revision_id: file.last_revision.id, + back_url: back_url), + title: l(:title_waiting_for_approval), + remote: true, + class: 'icon-only icon-ok' %> + <% else %> + + <% end %> + <% else %> + + <% end %> + <% when DmsfWorkflow::STATE_ASSIGNED %> + <% if User.current && (file.last_revision.dmsf_workflow_assigned_by_user == User.current) && wf %> + <%= link_to sprite_icon('checked', ''), + start_dmsf_workflow_path(id: file.last_revision.dmsf_workflow_id, + dmsf_file_revision_id: file.last_revision.id, + back_url: back_url), + title: l(:label_dmsf_wokflow_action_start), + class: 'icon-only icon-ok' %> + <% else %> + + <% end %> + <% when DmsfWorkflow::STATE_APPROVED, DmsfWorkflow::STATE_REJECTED, DmsfWorkflow::STATE_OBSOLETE %> + + <% else %> + <% if workflows_available %> + <%= link_to sprite_icon('checked', ''), + dmsf_link_id ? + assign_dmsf_workflow_path(id: project.id, project_id: project.id, dmsf_link_id: dmsf_link_id, + back_url: back_url) : + assign_dmsf_workflow_path(id: project.id, project_id: project.id, + dmsf_file_revision_id: file.last_revision.id, + back_url: back_url), + title: l(:label_dmsf_wokflow_action_assign), + remote: true, + class: 'icon-only icon-ok' %> + <% else %> + + <% end %> + <% end %> +<% end %> diff --git a/app/views/dmsf_workflows/_assign.html.erb b/app/views/dmsf_workflows/_assign.html.erb new file mode 100644 index 00000000..1d37f949 --- /dev/null +++ b/app/views/dmsf_workflows/_assign.html.erb @@ -0,0 +1,42 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

    <%= l(:field_label_dmsf_workflow) %>

    +<% remote = params[:dmsf_link_id] || params[:attachment_id] %> +<%= form_tag({ controller: 'dmsf_workflows', action: 'assignment' }, method: :post, id: 'assignment-form', + remote: remote) do %> + <%= hidden_field_tag :dmsf_file_revision_id, params[:dmsf_file_revision_id] %> + <%= hidden_field_tag :dmsf_link_id, params[:dmsf_link_id] %> + <%= hidden_field_tag :attachment_id, params[:attachment_id] %> + <%= hidden_field_tag :back_url, params[:back_url] %> +

    + <%= label_tag 'workflow', "#{l(:link_workflow)}:" %> + <%= select_tag 'dmsf_workflow_id', dmsf_workflows_for_select(@project, nil)%> +

    + <% if (!remote) && User.current.allowed_to?(:manage_workflows, @project) %> +

    + <%= link_to sprite_icon('add', l(:label_dmsf_workflow_new)), new_dmsf_workflow_path(project_id: @project.id), + class: 'icon icon-add' %> +

    + <% end %> +

    + <%= submit_tag l(:button_submit), name: 'commit', onclick: 'hideModal(this);' %> + <%= submit_tag l(:button_cancel), name: 'commit', onclick: 'hideModal(this);' %> +

    +<% end %> diff --git a/app/views/dmsf_workflows/_log.html.erb b/app/views/dmsf_workflows/_log.html.erb new file mode 100644 index 00000000..6a6a9d22 --- /dev/null +++ b/app/views/dmsf_workflows/_log.html.erb @@ -0,0 +1,103 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

    <%= l(:title_dmsf_workflow_log) %>

    +

    + <% if revision %> +

    +
    + <%= label_tag 'workflow_name', l(:field_label_dmsf_workflow_name) %> + <% if User.current.allowed_to?(:manage_workflows, @dmsf_workflow.project) %> + <%= link_to @dmsf_workflow.name, dmsf_workflow_path(@dmsf_workflow) %> + <% else %> + <%= @dmsf_workflow.name %> + <% end %> +
    +
    + <%= label_tag 'workflow_status', l(:field_status) %> + <%= revision.workflow_str false %> +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <%# SQLite3::SQLException: RIGHT and FULL OUTER JOINs are not currently supported %> + <%# sql = "SELECT c.action, c.note, c.created_at, c.author_id, a.user_id, s.step, s.name FROM dmsf_workflow_step_actions c RIGHT JOIN dmsf_workflow_step_assignments a ON a.id = c.dmsf_workflow_step_assignment_id RIGHT JOIN dmsf_workflow_steps s ON s.id = a.dmsf_workflow_step_id WHERE a.dmsf_file_revision_id = #{revision.id} ORDER BY s.step, s.id, c.action DESC, c.created_at" %> + <% sql = "SELECT c.action, c.note, c.created_at, c.author_id, a.user_id, s.step, s.name FROM dmsf_workflow_steps s LEFT OUTER JOIN dmsf_workflow_step_assignments a ON s.id = a.dmsf_workflow_step_id LEFT OUTER JOIN dmsf_workflow_step_actions c ON a.id = c.dmsf_workflow_step_assignment_id WHERE a.dmsf_file_revision_id = #{revision.id} ORDER BY s.step, s.id, c.action DESC, c.created_at" %> + <% result = DmsfWorkflowStep.connection.exec_query sql %> + <% last_step = 0 %> + <% result.each_with_index do |row, i| %> + + + <% last_step = row['step'] %> + + + + + + + + <% end %> + +
    <%= l(:label_dmsf_workflow_step) %><%= l(:label_user) %><%= l(:label_action) %><%= l(:link_workflow) %><%= l(:label_note) %><%= l(:label_date)%>
    <%= DmsfWorkflowStepAction.workflow_str(0) %>
    <%= link_to_user revision.dmsf_workflow_assigned_by_user if revision.dmsf_workflow_assigned_by_user %><%= DmsfWorkflowStepAction.action_str(DmsfWorkflowStepAction::ACTION_ASSIGN) %><%= DmsfWorkflowStepAction.workflow_str(DmsfWorkflowStepAction::ACTION_ASSIGN) %><%= format_time(revision.dmsf_workflow_assigned_at) if revision.dmsf_workflow_assigned_at %>
    <%= link_to_user revision.dmsf_workflow_started_by_user if revision.dmsf_workflow_started_by_user %><%= DmsfWorkflowStepAction.action_str(DmsfWorkflowStepAction::ACTION_START) %><%= DmsfWorkflowStepAction.workflow_str(DmsfWorkflowStepAction::ACTION_START) if revision.dmsf_workflow_started_by_user %><%= format_time(revision.dmsf_workflow_started_at) if revision.dmsf_workflow_started_at %>
    <%= row['step'] unless row['step'] == last_step %><%= row['name'] %><%= link_to_user User.find_by(id: row['author_id'].present? ? row['author_id'] : row['user_id']) %><%= DmsfWorkflowStepAction.action_str(row['action']) %> + <% 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']) %> + <% end %> + <%= row['note'] %><%= format_time(row['created_at']) if row['created_at'].present? %>
    +
    + <% end %> +

    diff --git a/app/views/dmsf_workflows/_main.html.erb b/app/views/dmsf_workflows/_main.html.erb new file mode 100644 index 00000000..68faf8a6 --- /dev/null +++ b/app/views/dmsf_workflows/_main.html.erb @@ -0,0 +1,85 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% if @project %> + <% @status = params[:status] || 1 %> + <% @workflows = DmsfWorkflow.status(@status).where(project_id: @project.id).sorted %> + <% @path = settings_project_path(@project, tab: 'dmsf_workflow') %> +

    + <%= 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" } %> +

    +<% else %> +
    + <%= 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" } %> +
    +

    <%= l(:label_dmsf_workflow_plural) %>

    +<% end %> + +<%= form_tag(@path, method: :get) do %> +
    + <%= l(:label_filter_plural) %> + + <% + worflows_count_by_status = DmsfWorkflow.where(project_id: @project&.id).group(:status).count.to_hash + options = options_for_select([[l(:label_all), ''], + ["#{l(:status_active)} (#{worflows_count_by_status[DmsfWorkflow::STATUS_ACTIVE].to_i})", + DmsfWorkflow::STATUS_ACTIVE.to_s], + ["#{l(:status_locked)} (#{worflows_count_by_status[DmsfWorkflow::STATUS_LOCKED].to_i})", + DmsfWorkflow::STATUS_LOCKED.to_s]], @status.to_s) + %> + <%= select_tag 'status', options, class: 'small', onchange: 'this.form.submit(); return false;' %> +
    +<% end %>  + +<% if @workflows.any? %> + + + + + + + <% @workflows.each do |workflow| %> + + + + + <% end %> + +
    <%= l(:field_name) %>
    <%= link_to(h(workflow.name), dmsf_workflow_path(workflow)) %> + <% url = { controller: 'dmsf_workflows', action: 'update', id: workflow.id } %> + <% if workflow.locked? %> + <%= link_to sprite_icon('unlock', l(:button_unlock)), + url.merge(dmsf_workflow: { status: DmsfWorkflow::STATUS_ACTIVE }), method: :put, + class: 'icon icon-unlock' %> + <% else %> + <%= link_to sprite_icon('lock', l(:button_lock)), + url.merge(dmsf_workflow: { status: DmsfWorkflow::STATUS_LOCKED }), method: :put, + class: 'icon icon-lock' %> + <% end %> + <%= delete_link dmsf_workflow_path(workflow, back_url: @path) %> +
    +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> + +<% unless @project %> + <%= pagination_links_full @workflow_pages %> +<% end %> diff --git a/app/views/dmsf_workflows/_new_step_modal.html.erb b/app/views/dmsf_workflows/_new_step_modal.html.erb new file mode 100644 index 00000000..eb629aee --- /dev/null +++ b/app/views/dmsf_workflows/_new_step_modal.html.erb @@ -0,0 +1,47 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

    <%= l(:dmsf_new_step) %>

    + +<%= labelled_form_for(@dmsf_workflow, { url: edit_dmsf_workflow_path(@dmsf_workflow), method: :post }) do |f| %> + <%= hidden_field_tag :back_url, dmsf_workflow_path(@dmsf_workflow) %> +
    + <%= l(:label_dmsf_workflow_add_approver) %> +

    <%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %>

    + <%= javascript_tag "observeSearchfield('user_search', null, '#{ escape_javascript autocomplete_for_user_dmsf_workflow_path(@dmsf_workflow, dmsf_workflow_step_assignment_id: nil, dmsf_file_revision_id: nil, project_id: @project ? @project.id : nil) }')" %> +
    + <%= render_principals_for_new_dmsf_workflow_users @dmsf_workflow %> +
    +
    +
    + <%= l(:label_dmsf_workflow_step) %> + <%= select_tag 'step', dmsf_workflow_steps_options_for_select(@steps), + id: 'selected_step', onchange: "$('#dmsf_step_name').toggle(this.value == 0);", style: 'max-width: 40%' %> +   + <%= label_tag 'name', l(:field_name) %> + <%= text_field_tag 'name', nil, maxlength: 30, style: 'max-width: 40%' %> + +
    +

    + <%= l(:label_add_width) %> + <%= f.submit l(:dmsf_and), id: 'add-step-and' %> + <%= f.submit l(:dmsf_or), id: 'add-step-or' %> + <%= link_to_function l(:button_cancel), 'hideModal(this);' %> +

    +<% end %> diff --git a/app/views/dmsf_workflows/_steps.html.erb b/app/views/dmsf_workflows/_steps.html.erb new file mode 100644 index 00000000..4b0b9b11 --- /dev/null +++ b/app/views/dmsf_workflows/_steps.html.erb @@ -0,0 +1,138 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% path = @dmsf_workflow.project ? settings_project_path(@project, tab: 'dmsf_workflow') : dmsf_workflows_path %> +

    <%= link_to l(:label_dmsf_workflow_plural), path %> » <%=h @dmsf_workflow %>

    + +<%= labelled_form_for @dmsf_workflow do |f| %> + <%= hidden_field_tag :back_url, dmsf_workflow_path(@dmsf_workflow) %> + <%= error_messages_for 'workflow' %> +
    +

    + <%= f.text_field :name, required: true %> + <%= f.submit l(:button_save) %> +

    +
    +<% end %> + +
    +

    + <%= link_to sprite_icon('add', l(:dmsf_new_step_or_approver)), new_step_dmsf_workflow_path(@dmsf_workflow), + remote: true, class: 'icon icon-add' %> +

    + <% steps = @dmsf_workflow.dmsf_workflow_steps.collect{|s| s.step}.uniq %> + <% if steps.any? %> + + + + + + + + + <% steps.each do |i| %> + + <% index = @dmsf_workflow.dmsf_workflow_steps.order(:id).index{ |s| s.step == i } %> + + + + + <% end %> + + +
    <%= l(:label_dmsf_workflow_step) %><%= l(:label_dmsf_workflow_approval_plural) %>
    <%= i %> + <%= @dmsf_workflow.dmsf_workflow_steps[index].name if index %> +
    "> + <%= form_for(@dmsf_workflow, url: update_step_dmsf_workflow_path(step: index), method: :put, + html: { id: "step-index-name-#{index}-form" }) do |f| %> + <%= hidden_field_tag :back_url, dmsf_workflow_path(@dmsf_workflow) %> + <%= f.text_field(:step_name, value: @dmsf_workflow.dmsf_workflow_steps[index].name, + id: "dmsf_workflow_step_name_#{index}") %> +

    + <%= submit_tag l(:button_change), class: 'small' %> + <%= link_to_function( + l(:button_cancel), + "$('#step-index-#{index}-name').show(); $('#step-index-#{index}-approvers').show(); $('#step-index-#{index}-name-form').hide(); $('#step-index-#{index}-approvers-form').hide();") + %> +

    + <% end %> +
    +
    + + <% stps = @dmsf_workflow.dmsf_workflow_steps.order(operator: :desc). + collect{ |s| (s.step == i) ? s : nil }.compact %> + <% stps.each_with_index do |step, j| %> + <% if (j > 0) || (step.operator != DmsfWorkflowStep::OPERATOR_AND) %> + <%= step.soperator %> + <% end %> + <%= link_to_user step.user %> + <% end %> + +
    "> + <%= form_for(@dmsf_workflow, url: update_step_dmsf_workflow_path(step: index), method: :put, + html: { id: "step-index-operator-#{index}-form"}) do |_| %> + <%= hidden_field_tag :back_url, dmsf_workflow_path(@dmsf_workflow) %> +
    + <% stps.each do |step| %> +
    + +
    + +
    +  <%= delete_link delete_step_dmsf_workflow_path(step: step.id) %> +
    +
    + <%= select_tag "assignee[#{step.id}]", + principals_options_for_select(@approving_candidates | [step.user], step.user), + include_blank: false %> +
    + <% end %> +
    +

    + <%= submit_tag l(:button_change), class: 'small' %> + <%= link_to_function l(:button_cancel), + "$('#step-index-#{index}-name').show(); $('#step-index-#{index}-approvers').show(); $('#step-index-#{index}-name-form').hide(); $('#step-index-#{index}-approvers-form').hide();" + %> +

    + <% end %> +
    +
    + <%= reorder_handle(@dmsf_workflow, url: url_for(action: 'edit', id: @dmsf_workflow, step: i) ) %> + <%= link_to_function sprite_icon('edit', l(:button_edit)), + "$('#step-index-#{index}-name').hide(); $('#step-index-#{index}-approvers').hide(); $('#step-index-#{index}-name-form').show(); $('#step-index-#{index}-approvers-form').show();", + class: 'icon icon-edit' %> + <%= delete_link edit_dmsf_workflow_path(@dmsf_workflow, step: i, + back_url: dmsf_workflow_path(@dmsf_workflow)) %> +
    + <% else %> +

    <%= l(:label_no_data) %>

    + <% end %> +
    + +<%= javascript_tag do %> + $(function() { $("table.steps tbody").positionedItems(); }); +<% end %> diff --git a/app/views/dmsf_workflows/action.js.erb b/app/views/dmsf_workflows/action.js.erb new file mode 100644 index 00000000..47b2b042 --- /dev/null +++ b/app/views/dmsf_workflows/action.js.erb @@ -0,0 +1,24 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +var modal = $('#ajax-modal'); + +modal.html('<%= escape_javascript(render partial: 'action', locals: { workflow: @dmsf_workflow }) %>'); +showModal('ajax-modal', '35%'); +modal.addClass('new-action'); diff --git a/app/views/dmsf_workflows/add_step.html.erb b/app/views/dmsf_workflows/add_step.html.erb new file mode 100644 index 00000000..83d20cea --- /dev/null +++ b/app/views/dmsf_workflows/add_step.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= render 'steps' %> diff --git a/app/views/dmsf_workflows/assign.js.erb b/app/views/dmsf_workflows/assign.js.erb new file mode 100644 index 00000000..db81832d --- /dev/null +++ b/app/views/dmsf_workflows/assign.js.erb @@ -0,0 +1,24 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +var modal = $('#ajax-modal'); + +modal.html('<%= escape_javascript(render partial: 'assign') %>'); +showModal('ajax-modal', '30%'); +modal.addClass('assignment'); diff --git a/app/views/dmsf_workflows/assignment.js.erb b/app/views/dmsf_workflows/assignment.js.erb new file mode 100644 index 00000000..f938e46f --- /dev/null +++ b/app/views/dmsf_workflows/assignment.js.erb @@ -0,0 +1,39 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% if @dmsf_link_id %> + var input = $('input[value="<%= @dmsf_link_id %>"]'); +<% else %> + var input = $('input[name="dmsf_attachments[<%= @attachment_id %>][token]"]'); +<% end %> + +var span = input.parent(); + +<% if @dmsf_link_id %> + span.append( + ""); +<% else %> + span.append( + ""); +<% end %> + +var icon = span.children("a.icon-ok"); + +icon.attr("href", "#"); +icon.attr("title", "<%= l(:title_assigned) %>"); diff --git a/app/views/dmsf_workflows/autocomplete_for_user.js.erb b/app/views/dmsf_workflows/autocomplete_for_user.js.erb new file mode 100644 index 00000000..2de2ea23 --- /dev/null +++ b/app/views/dmsf_workflows/autocomplete_for_user.js.erb @@ -0,0 +1,47 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +/* Get all checked users */ +var checkedBoxes = $('input[name="user_ids[]"]:checked'); + +/* Get all hidden tags */ +var hiddenTags = $('input[name="user_ids[]"]:hidden'); + +/* Draw the form */ +usersForDelegate = $('#dmsf_users_for_delegate'); + +usersForDelegate.html('<%= escape_javascript( + render_principals_for_new_dmsf_workflow_users( + @dmsf_workflow, params[:dmsf_workflow_step_assignment_id], params[:dmsf_file_revision_id])) %>'); + +/* Add all checked users from previous page as hidden tags */ +checkedBoxes.each(function() { + usersForDelegate.append(''); +}); + +/* Re-add all hidden tags and re-check displayed users */ +hiddenTags.each(function() { + let userCheckBox = $("input[value='" + $(this).val() + "']"); + if(userCheckBox.length) { + userCheckBox.prop('checked', true); + } + else { + usersForDelegate.append(''); + } +}); diff --git a/app/views/dmsf_workflows/index.html.erb b/app/views/dmsf_workflows/index.html.erb new file mode 100644 index 00000000..5346eba3 --- /dev/null +++ b/app/views/dmsf_workflows/index.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= render 'main' %> diff --git a/app/views/dmsf_workflows/log.html.erb b/app/views/dmsf_workflows/log.html.erb new file mode 100644 index 00000000..32708b63 --- /dev/null +++ b/app/views/dmsf_workflows/log.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= render partial: 'log', locals: { workflow: @dmsf_workflow, revision: @revision } %> diff --git a/app/views/dmsf_workflows/log.js.erb b/app/views/dmsf_workflows/log.js.erb new file mode 100644 index 00000000..abe65d62 --- /dev/null +++ b/app/views/dmsf_workflows/log.js.erb @@ -0,0 +1,24 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +var modal = $('#ajax-modal'); + +modal.html('<%= escape_javascript(render partial: 'log', locals: { workflow: @dmsf_workflow, revision: @revision }) %>'); +showModal('ajax-modal', '90%'); +modal.addClass('workflow-log'); diff --git a/app/views/dmsf_workflows/new.html.erb b/app/views/dmsf_workflows/new.html.erb new file mode 100644 index 00000000..b6d98dbe --- /dev/null +++ b/app/views/dmsf_workflows/new.html.erb @@ -0,0 +1,53 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% if !@project && params[:dmsf_workflow] && params[:dmsf_workflow][:project_id].present? %> + <% @project = Project.find_by_id params[:dmsf_workflow][:project_id] %> +<% end %> +<% if @project %> +

    + <%= link_to l(:label_dmsf_workflow_plural), settings_project_path(@project, tab: 'dmsf_workflow') %> » <%= l(:label_dmsf_workflow_new) %> +

    +<% else %> +

    <%= link_to l(:label_dmsf_workflow_plural), dmsf_workflows_path %> » <%= l(:label_dmsf_workflow_new) %>

    +<% end %> + +<%= labelled_form_for @dmsf_workflow do |f| %> + <%= f.hidden_field(:project_id, value: @project.id) if @project %> + <%= error_messages_for 'dmsf_workflow' %> +
    +

    + <%= f.text_field :name, required: true %> +

    +

    + <%= f.select :id, dmsf_all_workflows_for_select(params[:dmsf_workflow] ? params[:dmsf_workflow][:id] : nil), + label: l(:label_copy_workflow_from) %> +

    +
    + <%= f.submit l(:button_create), class: 'button-positive' %> +
    +
    +<% end %> + +<%= javascript_tag do %> + $('#dmsf_workflow_id').change(function () { + $('#content').load("<%= @project ? url_for(action: 'new', project_id: @project.id) : url_for(action: 'new') %>", $('#new_dmsf_workflow').serialize()); + }); + $('#dmsf_workflow_id').select2(); +<% end %> diff --git a/app/views/dmsf_workflows/new_step.js.erb b/app/views/dmsf_workflows/new_step.js.erb new file mode 100644 index 00000000..1eceaac7 --- /dev/null +++ b/app/views/dmsf_workflows/new_step.js.erb @@ -0,0 +1,21 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +$('#ajax-modal').html('<%= escape_javascript(render partial: 'new_step_modal') %>'); +showModal('ajax-modal', '40%'); diff --git a/app/views/dmsf_workflows/remove_step.html.erb b/app/views/dmsf_workflows/remove_step.html.erb new file mode 100644 index 00000000..83d20cea --- /dev/null +++ b/app/views/dmsf_workflows/remove_step.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= render 'steps' %> diff --git a/app/views/dmsf_workflows/reorder_steps.html.erb b/app/views/dmsf_workflows/reorder_steps.html.erb new file mode 100644 index 00000000..83d20cea --- /dev/null +++ b/app/views/dmsf_workflows/reorder_steps.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= render 'steps' %> diff --git a/app/views/dmsf_workflows/reorder_steps.js.erb b/app/views/dmsf_workflows/reorder_steps.js.erb new file mode 100644 index 00000000..6fdcb6a9 --- /dev/null +++ b/app/views/dmsf_workflows/reorder_steps.js.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +location.replace('<%= dmsf_workflow_path(@dmsf_workflow) %>'); diff --git a/app/views/dmsf_workflows/show.html.erb b/app/views/dmsf_workflows/show.html.erb new file mode 100644 index 00000000..83d20cea --- /dev/null +++ b/app/views/dmsf_workflows/show.html.erb @@ -0,0 +1,20 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= render 'steps' %> diff --git a/app/views/hooks/redmine_dmsf/_view_layouts_base_html_head.html.erb b/app/views/hooks/redmine_dmsf/_view_layouts_base_html_head.html.erb new file mode 100644 index 00000000..768921aa --- /dev/null +++ b/app/views/hooks/redmine_dmsf/_view_layouts_base_html_head.html.erb @@ -0,0 +1,26 @@ +<% + # encoding: utf-8 + # + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%= stylesheet_link_tag('redmine_dmsf', plugin: :redmine_dmsf) %> +<%= stylesheet_link_tag('select2.min', plugin: :redmine_dmsf) %> +<%= javascript_include_tag('select2.min', plugin: :redmine_dmsf, defer: true) %> +<%= javascript_include_tag('redmine_dmsf', plugin: :redmine_dmsf, defer: true) %> +<%= javascript_include_tag('attachments_dmsf', plugin: :redmine_dmsf, defer: true) %> diff --git a/app/views/hooks/redmine_dmsf/_view_mailer_issue.html.erb b/app/views/hooks/redmine_dmsf/_view_mailer_issue.html.erb new file mode 100644 index 00000000..44168786 --- /dev/null +++ b/app/views/hooks/redmine_dmsf/_view_mailer_issue.html.erb @@ -0,0 +1,28 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% if issue.dmsf_files.any? %> +
    +
    <%= l(:label_dmsf_file_plural) %> + <% issue.dmsf_files.each do |f| %> + <%= link_to h(f.name), static_dmsf_file_url(f, filename: f.name) %> + (<%= number_to_human_size(f.last_revision.size) if f.last_revision %>)
    + <% end %> +
    +<% end %> diff --git a/app/views/hooks/redmine_dmsf/_view_mailer_issue.text.erb b/app/views/hooks/redmine_dmsf/_view_mailer_issue.text.erb new file mode 100644 index 00000000..ddb9ba88 --- /dev/null +++ b/app/views/hooks/redmine_dmsf/_view_mailer_issue.text.erb @@ -0,0 +1,25 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% if issue.dmsf_files.any? %> + ---<%= l(:label_dmsf_file_plural).ljust(37, '-') %> + <% issue.dmsf_files.each do |f| %> + <%= f.name %> (<%= number_to_human_size(f.last_revision.size) if f.last_revision %>) + <% end %> +<% end %> diff --git a/app/views/hooks/redmine_dmsf/_view_my_account.html.erb b/app/views/hooks/redmine_dmsf/_view_my_account.html.erb new file mode 100644 index 00000000..f4f88e42 --- /dev/null +++ b/app/views/hooks/redmine_dmsf/_view_my_account.html.erb @@ -0,0 +1,42 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<%= labelled_fields_for :pref, @user.pref do |pref_fields| %> +

    + <% options = [[l(:label_none), nil]] %> + <% options.concat DmsfQuery.only_public.where(project_id: nil).or(DmsfQuery.where(user_id: @user.id)). + pluck(:name, :id) %> + <%= pref_fields.select :default_dmsf_query, + options_for_select(options, selected: @user.pref.default_dmsf_query) %> +

    +

    <%= pref_fields.check_box :receive_download_notification %>

    +<% end %> +<% if RedmineDmsf.dmsf_webdav_authentication == 'Digest' %> +

    + + <% token = Token.find_by(user_id: @user.id, action: 'dmsf_webdav_digest') %> + <% if token %> + <%= l(:label_dmsf_webdav_digest_created_on, distance_of_time_in_words(Time.now, token.created_on)) %> + (<%= link_to l(:button_reset), dmsf_digest_path, remote: true, id: 'webdav_digest_reset' %>) + <% else %> + <%= l(:label_missing_dmsf_webdav_digest) %> + (<%= link_to l(:button_add), dmsf_digest_path, remote: true, id: 'webdav_digest_reset' %>) + <% end %> +

    +<% end %> diff --git a/app/views/hooks/redmine_dmsf/_view_projects_form.html.erb b/app/views/hooks/redmine_dmsf/_view_projects_form.html.erb new file mode 100644 index 00000000..986f358d --- /dev/null +++ b/app/views/hooks/redmine_dmsf/_view_projects_form.html.erb @@ -0,0 +1,41 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + +<% if @project.new_record? && @source_project %> +

    + <% counts = @source_project.dmsf_count %> + + + +

    +<% end %> diff --git a/app/views/layouts/_document.html.erb b/app/views/layouts/_document.html.erb new file mode 100644 index 00000000..8dc26804 --- /dev/null +++ b/app/views/layouts/_document.html.erb @@ -0,0 +1,46 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +
    + <%= 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), + class: 'icon icon-download', disabled: false %> +
    + +

    + <%= render partial: '/dmsf/path', locals: { folder: @file.dmsf_folder, filename: @file.title, title: nil } %> +

    + +
    +

    + <%= @file.description %> + + <%= link_to_user @file.last_revision.user %>, <%= format_time @file.last_revision.updated_at %> + +

    +
    +
    + <%= yield %> +
    + +<% html_title @file.name %> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/mailer/_issue.html.erb b/app/views/mailer/_issue.html.erb new file mode 100644 index 00000000..497fc3f8 --- /dev/null +++ b/app/views/mailer/_issue.html.erb @@ -0,0 +1,42 @@ +<% + # encoding: utf-8 + # + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%# The original Redmine code %> +<%# TODO: Render the original template instead %> +

    + <%= link_to("#{issue.tracker.name} ##{issue.id}: #{issue.subject}", issue_url) %> + <%= issue_status_type_badge(issue.status) %> +

    + +<%= render_email_issue_attributes(issue, user, true) %> + +<%= textilizable(issue, :description, :only_path => false) %> + +<% if issue.attachments.any? %> +
    <%= l(:label_attachment_plural) %> + <% issue.attachments.each do |attachment| %> + <%= link_to_attachment attachment, :download => true, :only_path => false %> + (<%= number_to_human_size(attachment.filesize) %>)
    + <% end %> +
    +<% end %> +<%# DMSF extension %> +<%= render partial: 'hooks/redmine_dmsf/view_mailer_issue', locals: { issue: issue } %> diff --git a/app/views/mailer/_issue.text.erb b/app/views/mailer/_issue.text.erb new file mode 100644 index 00000000..9a3ed22b --- /dev/null +++ b/app/views/mailer/_issue.text.erb @@ -0,0 +1,38 @@ +<% + # encoding: utf-8 + # + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<%# The original Redmine code %> +<%# TODO: Render the original template instead %> +<%= "#{issue.tracker.name} ##{issue.id}: #{issue.subject}" %> +<%= issue_url %> + +<%= render_email_issue_attributes(issue, user) %> +---------------------------------------- +<%= issue.description %> + +<% if issue.attachments.any? -%> + ---<%= l(:label_attachment_plural).ljust(37, '-') %> + <% issue.attachments.each do |attachment| -%> + <%= attachment.filename %> (<%= number_to_human_size(attachment.filesize) %>) + <% end -%> +<% end -%> +<%# DMSF extension %> +<%= render partial: 'hooks/redmine_dmsf/view_mailer_issue', locals: { issue: issue } %> diff --git a/app/views/my/blocks/_locked_documents.html.erb b/app/views/my/blocks/_locked_documents.html.erb new file mode 100644 index 00000000..5288d5f7 --- /dev/null +++ b/app/views/my/blocks/_locked_documents.html.erb @@ -0,0 +1,105 @@ +<% + # encoding: utf-8 + # + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% folders = DmsfFolder.visible.joins( + 'JOIN dmsf_locks ON dmsf_folders.id = dmsf_locks.entity_id').where( + ['dmsf_locks.entity_type = ? AND dmsf_locks.user_id = ? AND (dmsf_locks.expires_at IS NULL OR dmsf_locks.expires_at > ?)', + 1, @user&.id, Time.current]) %> +<% files = DmsfFile.visible.joins( + 'JOIN dmsf_locks ON dmsf_files.id = dmsf_locks.entity_id').where( + ['dmsf_locks.entity_type = ? AND dmsf_locks.user_id = ? AND (dmsf_locks.expires_at IS NULL OR dmsf_locks.expires_at > ?)', + 0, @user&.id, Time.current]) %> +

    + <%= l(:locked_documents)%> (<%= "#{folders.all.size} #{l(:label_number_of_folders).downcase}" %> / <%= "#{files.all.size} #{l(:label_number_of_documents).downcase}" %>) +

    +<% if folders.any? || files.any?%> + <%= form_tag({}, data: { cm_url: dmsf_context_menu_path }) do %> + <%= hidden_field_tag 'back_url', my_page_path, id: nil %> + + + + + + + + + + + + <% folders.each do |folder| %> + + + + + + + + <% end %> + <% files.each do |file| %> + + + + + + + + <% end %> + +
    + <%= check_box_tag 'check_all', '', false, class: 'toggle-selection', + title: "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> + <%= l(:field_project) %><%= l(:label_document) %>/<%= l(:field_folder) %><%= l(:field_folder) %>
    + <%= check_box_tag 'ids[]', "folder-#{folder.id}", false, id: nil %> + + <%= link_to_project folder.project %> + + <%= link_to sprite_icon('folder', h(folder.title)), + dmsf_folder_path(id: folder.project, folder_id: folder), class: 'icon icon-folder' %> + + <% if folder.dmsf_folder %> + <%= link_to h(folder.dmsf_folder.title), + dmsf_folder_path(id: folder.project, folder_id: folder.dmsf_folder) %> + <% else %> + <%= link_to l(:link_documents), dmsf_folder_path(id: folder.project) %> + <% end %> + + <%= link_to_context_menu %> +
    + <%= check_box_tag 'ids[]', "file-#{file.id}", false, id: nil %> + + <%= link_to_project file.project %> + + <% icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(file.name)) %> + <%= link_to sprite_icon(icon_name, h(file.title)), dmsf_file_path(id: file), class: 'icon icon-file' %> + + <% if file.dmsf_folder %> + <%= link_to h(file.dmsf_folder.title), dmsf_folder_path(id: file.project, folder_id: file.dmsf_folder) %> + <% else %> + <%= link_to_if file.project, l(:link_documents), dmsf_folder_path(id: file.project) %> + <% end %> + + <%= link_to_context_menu %> +
    + <% end %> + <%= context_menu %> +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> diff --git a/app/views/my/blocks/_open_approvals.html.erb b/app/views/my/blocks/_open_approvals.html.erb new file mode 100644 index 00000000..d52e6d2a --- /dev/null +++ b/app/views/my/blocks/_open_approvals.html.erb @@ -0,0 +1,110 @@ +<% + # encoding: utf-8 + # + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% all_assignments = DmsfWorkflowStepAssignment. + left_outer_joins(:dmsf_workflow_step_actions). + joins(:dmsf_workflow_step). + joins(:dmsf_file_revision). + where(dmsf_file_revisions: { deleted: DmsfFileRevision::STATUS_ACTIVE, workflow: DmsfWorkflow::STATE_WAITING_FOR_APPROVAL }). + where(dmsf_workflow_step_assignments: { user_id: @user&.id }). + where(['dmsf_workflow_step_actions.id IS NULL OR dmsf_workflow_step_actions.action = ?', + DmsfWorkflowStepAction::ACTION_DELEGATE]). + order('dmsf_workflow_step_assignments.dmsf_file_revision_id, dmsf_workflow_steps.step').to_a %> +<% assignments = all_assignments.delete_if { |a| (a.dmsf_file_revision != a.dmsf_file_revision.dmsf_file.last_revision) } %> +<% assignments.uniq! { |a| a.dmsf_file_revision } %> +

    <%= l(:open_approvals)%> (<%= assignments.size %>)

    +<% if assignments.any? %> + <%= form_tag({}, data: { cm_url: dmsf_context_menu_path }) do %> + <%= hidden_field_tag 'back_url', my_page_path, id: nil %> + + + + + + + + + + + + + + <% assignments.each do |assignment| %> + + + + + + + + + + <% end %> + +
    + <%= check_box_tag 'check_all', '', false, class: 'toggle-selection', + title: "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> + <%= l(:field_project) %><%= l(:field_label_dmsf_workflow) %><%= l(:field_status) %><%= l(:label_document) %><%= l(:field_folder) %>
    + <%= check_box_tag 'ids[]', "file-#{assignment.dmsf_file_revision.dmsf_file.id}", false, id: nil %> + + <% if assignment.dmsf_file_revision.dmsf_file.project %> + <%= link_to_project assignment.dmsf_file_revision.dmsf_file.project %> + <% end %> + + <% if assignment.dmsf_workflow_step && assignment.dmsf_workflow_step.dmsf_workflow %> + <%= link_to h(assignment.dmsf_workflow_step.dmsf_workflow.name), + edit_dmsf_workflow_path(assignment.dmsf_workflow_step.dmsf_workflow) %> + <% end %> + + <% if assignment.dmsf_workflow_step.dmsf_workflow %> + <%= link_to assignment.dmsf_file_revision.workflow_str(false), + log_dmsf_workflow_path( + project_id: assignment.dmsf_file_revision.dmsf_file.project_id, + id: assignment.dmsf_workflow_step.dmsf_workflow_id, + dmsf_file_revision_id: assignment.dmsf_file_revision_id), + title: assignment.dmsf_file_revision.workflow_tooltip, + remote: true %> + <% else %> + <%= assignment.dmsf_file_revision.workflow_str(false) %> + <% end %> + + <% if assignment.dmsf_file_revision && assignment.dmsf_file_revision.dmsf_file %> + <%= link_to h(assignment.dmsf_file_revision.title), + dmsf_file_path(id: assignment.dmsf_file_revision.dmsf_file) %> + <% end %> + + <% if assignment.dmsf_file_revision %> + <% if assignment.dmsf_file_revision.dmsf_file.dmsf_folder %> + <%= link_to h(assignment.dmsf_file_revision.dmsf_file.dmsf_folder.title), + dmsf_folder_path(id: assignment.dmsf_file_revision.dmsf_file.project, + folder_id: assignment.dmsf_file_revision.dmsf_file.dmsf_folder) %> + <% elsif assignment.dmsf_file_revision.dmsf_file.project %> + <%= link_to l(:link_documents), dmsf_folder_path(id: assignment.dmsf_file_revision.dmsf_file.project) %> + <% end %> + <% end %> + + <%= link_to_context_menu %> +
    + <% end %> + <%= context_menu %> +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> diff --git a/app/views/my/blocks/_watched_documents.html.erb b/app/views/my/blocks/_watched_documents.html.erb new file mode 100644 index 00000000..2bb9c4da --- /dev/null +++ b/app/views/my/blocks/_watched_documents.html.erb @@ -0,0 +1,118 @@ +<% + # encoding: utf-8 + # + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . + %> + +<% projects = Project.visible.has_module(:dmsf).joins(:watchers).where( + :watchers => { watchable_type: 'Project', user_id: @user&.id }) %> +<% folders = DmsfFolder.visible(:dmsf).joins(:watchers).where( + :watchers => { watchable_type: 'DmsfFolder', user_id: @user&.id }) %> +<% files = DmsfFile.visible.joins(:watchers).where( + :watchers => { watchable_type: 'DmsfFile', user_id: @user&.id }) %> +

    <%= l(:label_dmsf_watched)%> (<%= projects.all.size + folders.all.size %>/<%= files.all.size %>)

    +<% if projects.any? || folders.any? || files.any? %> + <%= form_tag({}, data: { cm_url: dmsf_context_menu_path }) do %> + <%= hidden_field_tag 'back_url', my_page_path, id: nil %> + + + + + + + + + + + + <% projects.each do |project| %> + + + + + + + + <% end %> + <% folders.each do |folder| %> + + + + + + + + <% end %> + <% files.each do |file| %> + + + + + + + + <% end %> + +
    + <%= check_box_tag 'check_all', '', false, class: 'toggle-selection', + title: "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> + <%= l(:field_project) %><%= l(:label_document) %>/<%= l(:field_folder) %><%= l(:field_folder) %>
    + <%= check_box_tag 'ids[]', "project-#{project.id}", false, id: nil %> + + <%= link_to_project project %> + + <%= link_to_project project, jump: 'dmsf', class: 'project' %> + + <%= link_to_context_menu %> +
    + <%= check_box_tag 'ids[]', "folder-#{folder.id}", false, id: nil %> + + <%= link_to_project folder.project %> + + <%= link_to sprite_icon('folder', h(folder.title)), + dmsf_folder_path(id: folder.project, folder_id: folder), class: 'icon icon-folder' %> + + <% if folder.dmsf_folder %> + <%= link_to h(folder.dmsf_folder.title), dmsf_folder_path(id: folder.project, folder_id: + folder.dmsf_folder) %> + <% else %> + <%= link_to l(:link_documents), dmsf_folder_path(id: folder.project) %> + <% end %> + + <%= link_to_context_menu %> +
    + <%= check_box_tag 'ids[]', "file-#{file.id}", false, id: nil %> + + <%= link_to_project(file.project) if file.project %> + + <% icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(file.name)) %> + <%= link_to sprite_icon(icon_name, h(file.title)), dmsf_file_path(id: file), class: 'icon icon-file' %> + + <% if file.dmsf_folder %> + <%= link_to h(file.dmsf_folder.title), dmsf_folder_path(id: file.project, folder_id: file.dmsf_folder) %> + <% else %> + <%= link_to l(:link_documents), dmsf_folder_path(id: file.project) %> + <% end %> + + <%= link_to_context_menu %> +
    + <% end %> + <%= context_menu %> +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> diff --git a/app/views/search/_container.html.erb b/app/views/search/_container.html.erb new file mode 100644 index 00000000..987bf0c9 --- /dev/null +++ b/app/views/search/_container.html.erb @@ -0,0 +1,28 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +<% dmsf_file_or_folder = object %> +<% if dmsf_file_or_folder.dmsf_folder_id %> + <% titles = DmsfFolder.where(id: dmsf_file_or_folder.dmsf_folder_id).pluck(:title) %> + <% title = titles.first %> +<% else %> + <% title = dmsf_file_or_folder.project.name %> +<% end %> +<%= link_to h(title), dmsf_folder_path(id: dmsf_file_or_folder.project, + folder_id: dmsf_file_or_folder.dmsf_folder_id), class: 'icon icon-folder' %> diff --git a/app/views/settings/_dmsf_columns.html.erb b/app/views/settings/_dmsf_columns.html.erb new file mode 100644 index 00000000..591dddc0 --- /dev/null +++ b/app/views/settings/_dmsf_columns.html.erb @@ -0,0 +1,42 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Karel Pičman + # + # 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 + # . +%> + +

    + <%= content_tag(:label, l(:field_column_names)) %> + <% columns = DmsfFolder::AVAILABLE_COLUMNS.dup %> + <% columns.concat(extra_columns) if defined?(extra_columns) %> + <% index = columns.index(l(:field_project)) %> + <%# Move Project to the second position %> + <% if index %> + <% columns.insert(0, columns.delete_at(index)) %> + <% end %> + <% cfs = DmsfFileRevisionCustomField.visible.order(:position) %> + <% selected_columns = DmsfFolder::DEFAULT_COLUMNS if selected_columns.blank? %> + <% columns.each_with_index do |column, i| %> + <%= check_box_tag 'settings[dmsf_columns][]', column, selected_columns.include?(column), id: "dmsf_column_#{i}" %> + <%= l("label_column_#{column}").capitalize %> +
    + <% end %> + <% columns = cfs.map{ |c| c.name } + columns.each_with_index do |column, i| %> + <%= check_box_tag 'settings[dmsf_columns][]', column, selected_columns.include?(column), id: "dmsf_column_#{i}" %> + <%= h column.capitalize %> +
    + <% end %> +

    diff --git a/app/views/settings/_dmsf_settings.html.erb b/app/views/settings/_dmsf_settings.html.erb new file mode 100644 index 00000000..47968b35 --- /dev/null +++ b/app/views/settings/_dmsf_settings.html.erb @@ -0,0 +1,378 @@ +<% + # Redmine plugin for Document Management System "Features" + # + # Vít Jonáš , Daniel Munn , Karel Pičman + # + # 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 + # . +%> + + + <%= l(:label_general) %> + + +

    + <%= content_tag :label, l(:label_maximum_files_download) %> + <%= text_field_tag 'settings[dmsf_max_file_download]', RedmineDmsf.dmsf_max_file_download, size: 10 %> + + <%= l(:note_maximum_number_of_files_downloaded) %>
    + <%= l(:label_default) %>: 0 +
    +

    + +

    + <%= content_tag :label, l(:label_dmsf_max_notification_receivers_info) %> + <%= text_field_tag 'settings[dmsf_max_notification_receivers_info]', RedmineDmsf.dmsf_max_notification_receivers_info, + size: 10 %> + + <%= l(:note_dmsf_max_notification_receivers_info) %>
    + <%= l(:label_default) %>: 10 +
    +

    + +

    + <%= content_tag :label, l(:label_maximum_email_filesize) %> + <%= text_field_tag 'settings[dmsf_max_email_filesize]', RedmineDmsf.dmsf_max_email_filesize, size: 10 %> + + <%= l(:note_maximum_email_filesize) %>
    + <%= l(:label_default) %>: 0 +
    +

    + +

    + <%= content_tag :label, l(:label_file_storage_directory) %> + <%= text_field_tag 'settings[dmsf_storage_directory]', RedmineDmsf.dmsf_storage_directory, size: 256 %> + + <%= l(:label_default) %>: files/dmsf + +

    +<% unless File.exist?(DmsfFile.storage_path) %> + <% begin %> + <% FileUtils.mkdir_p DmsfFile.storage_path %> + <% rescue %> +

    <%= l(:error_file_storage_directory_does_not_exist) %>

    + <% end %> +<% end %> +<% testfilename = DmsfFile.storage_path.join('test.test') %> +<% if File.exist?(RedmineDmsf.dmsf_storage_directory) %> + <% begin %> + <% File.open(testfilename, 'wb') {} %> + <% rescue %> +

    <%= l(:error_file_can_not_be_created) %>

    + <% ensure %> + <% FileUtils.rm_f testfilename %> + <% end %> +<% end %> + +

    + <%= content_tag :label, l(:label_dmsf_office_bin) %> + <%= text_field_tag 'settings[office_bin]', RedmineDmsf.office_bin, size: 10 %> + <% unless RedmineDmsf::Preview.office_available? %> + + <%= l(:note_dmsf_office_bin_not_available, RedmineDmsf.office_bin ||= 'libreoffice') %> + + <% end %> + + <%= l(:note_dmsf_office_bin) %>
    + <%= l(:label_default) %>: 'libreoffice' +
    +

    + +

    + <%= content_tag :label, l(:label_physical_file_delete) %> + <%= check_box_tag 'settings[dmsf_really_delete_files]', '1', RedmineDmsf.physical_file_delete? %> + + <%= l(:label_default)%>: <%= l(:general_text_No)%> + +

    + +

    + <%= content_tag :label, l(:label_default_notifications) %> + <%= select_tag 'settings[dmsf_default_notifications]', + options_for_select([ + [l(:select_option_deactivated), '0'], + [l(:select_option_activated), '1']], + selected: RedmineDmsf.dmsf_default_notifications? ? '1' : '0') %> + + <%= l(:label_default) %>: <%= l(:select_option_deactivated) %> + +

    + +

    + <%= content_tag :label, l(:label_display_notified_recipients) %> + <%= select_tag 'settings[dmsf_display_notified_recipients]', + options_for_select([ + [l(:select_option_deactivated), '0'], + [l(:select_option_activated), '1']], + selected: RedmineDmsf.dmsf_display_notified_recipients? ? '1' : '0') %> + + <%= l(:note_display_notified_recipients) %>
    + <%= l(:label_default) %>: <%= l(:select_option_deactivated) %> +
    +

    + +

    + <%= content_tag :label, l(:label_title_format) %> + <%= text_field_tag 'settings[dmsf_global_title_format]', RedmineDmsf.dmsf_global_title_format, size: 10 %> + + <%= l(:text_title_format) %> + +

    + +

    + <%= content_tag :label, l(:label_act_as_attachable) %> + <%= check_box_tag 'settings[dmsf_act_as_attachable]', '1', RedmineDmsf.dmsf_act_as_attachable? %> + + <%= l(:note_dmsf_act_as_attachable) %>
    + <%= l(:label_default) %>: <%= l(:general_text_No) %> +
    +

    + +

    + <%= content_tag :label, l(:label_dmsf_projects_as_subfolders) %> + <%= check_box_tag 'settings[dmsf_projects_as_subfolders]', '1', RedmineDmsf.dmsf_projects_as_subfolders? %> + + <%= l(:note_dmsf_projects_as_subfolders) %>
    + <%= l(:label_default) %>: <%= l(:general_text_No) %> +
    +

    + +

    + <%= content_tag :label, l(:label_dmsf_global_menu_disabled) %> + <%= check_box_tag 'settings[dmsf_global_menu_disabled]', '1', RedmineDmsf.dmsf_global_menu_disabled? %> + + <%= l(:note_dmsf_global_menu_disabled) %>
    + <%= l(:label_default) %>: <%= l(:general_text_No) %> +
    +

    + +

    + <%= content_tag :label, l(:label_empty_minor_version_by_default) %> + <%= check_box_tag 'settings[empty_minor_version_by_default]', '1', RedmineDmsf.empty_minor_version_by_default? %> + + <%= l(:label_default) %>: <%= l(:general_text_No) %> + +

    + +

    + <%= content_tag :label, l(:label_remove_original_documents_module) %> + <%= check_box_tag 'settings[remove_original_documents_module]', '1', RedmineDmsf.remove_original_documents_module? %> + + <%= l(:label_default) %>: <%= l(:general_text_No) %> + +

    + +
    + + <%= l(:label_dmsf_columns) %> + + +<%= render partial: 'settings/dmsf_columns', locals: { selected_columns: RedmineDmsf.dmsf_columns } %> + +

    + <%= content_tag :label, l(:label_default_query) %> + <% options = [[l(:label_none), nil]] %> + <% options.concat DmsfQuery.only_public.where(project_id: nil).pluck(:name, :id) %> + <%= select_tag 'settings[dmsf_default_query]', + options_for_select(options, selected: RedmineDmsf.dmsf_default_query) %> +

    + +
    + + <%= l(:heading_send_documents_by_email) %> + + +

    + <%= content_tag :label, l(:label_email_from_override) %> + <%= text_field_tag 'settings[dmsf_documents_email_from]', RedmineDmsf.dmsf_documents_email_from, size: 128 %> + + <%= l(:label_default) %>: <%= l(:text_email_from_override) %> + +

    + +

    + <%= content_tag :label, l(:label_email_reply_to) %> + <%= text_field_tag 'settings[dmsf_documents_email_reply_to]', RedmineDmsf.dmsf_documents_email_reply_to, size: 128 %> + + <%= l(:label_default) %>: <%= "''" %> + +

    + +

    + <%= content_tag :label, l(:label_links_only).capitalize %> + <%= check_box_tag 'settings[dmsf_documents_email_links_only]', '1', RedmineDmsf.dmsf_documents_email_links_only? %> + + <%= l(:label_default) %>: <%= l(:general_text_No) %> + +

    + +
    + + <%= l(:field_label_dmsf_workflow) %> + + +

    + <%= content_tag :label, l(:label_dmsf_keep_documents_locked) %> + <%= check_box_tag 'settings[dmsf_keep_documents_locked]', '1', RedmineDmsf.dmsf_keep_documents_locked? %> + + <%= l(:note_dmsf_keep_documents_locked) %>
    + <%= l(:label_default)%>: <%= l(:general_text_No)%> +
    +

    + +

    + <%= content_tag(:label, l(:only_approval_zero_minor_version)) %> + <%= check_box_tag 'settings[only_approval_zero_minor_version]', '1', RedmineDmsf.only_approval_zero_minor_version? %> + + <%= l(:only_approval_zero_minor_version) %>
    <%= l(:label_default) %>: <%= l(:general_text_No)%> +
    +

    + +
    + + <%= l(:label_webdav) %> + + +

    + <%= content_tag :label, l(:label_webdav) %> + <%= select_tag 'settings[dmsf_webdav]', + options_for_select([ + [l(:select_option_deactivated), '0'], + [l(:select_option_activated), '1']], + selected: RedmineDmsf.dmsf_webdav? ? '1' : '0'), + onchange: "$('#dmsf_webdav_block').toggle()" %> + + <%= l(:note_webdav, protocol: Setting.protocol, domain: Setting.host_name) %>
    + <%= l(:label_default) %>: <%= l(:select_option_deactivated) %> +
    +

    + +<% visible_class = RedmineDmsf.dmsf_webdav? ? '' : 'dmsf-hidden' %> + +
    +

    + <%= content_tag :label, l(:label_webdav_authentication) %> + <% auth_types = [%w[Basic Basic], %w[Digest Digest]] %> + <%= select_tag 'settings[dmsf_webdav_authentication]', + options_for_select(auth_types, RedmineDmsf.dmsf_webdav_authentication) %> + + <%= l(:note_webdav_authentication) %>
    + <%= l(:label_default)%>: <%= auth_types[1][0] %> +
    +

    +

    + <%= content_tag :label, l(:label_webdav_strategy) %> + <%= select_tag'settings[dmsf_webdav_strategy]', + options_for_select([ + [l(:select_option_webdav_readonly), 'WEBDAV_READ_ONLY'], + [l(:select_option_webdav_readwrite), 'WEBDAV_READ_WRITE']], + selected: RedmineDmsf.dmsf_webdav_strategy), + onchange: "$('#dmsf_webdav_ro_block').toggle()" %>
    + + <%= l(:note_webdav_strategy) %>
    + <%= l(:label_default) %>: <%= l(:select_option_webdav_readonly) %> +
    +

    + + <% visible_class = (RedmineDmsf.dmsf_webdav_strategy == 'WEBDAV_READ_ONLY') ? 'dmsf-hidden' : '' %> + +
    +

    + <%= content_tag(:label, l(:label_webdav_ignore)) %> + <%= text_field_tag 'settings[dmsf_webdav_ignore]', RedmineDmsf.dmsf_webdav_ignore, size: 50 %> + + <%= l(:note_webdav_ignore) %> <%= l(:label_default) %>: ^(\._|\.DS_Store$|Thumbs.db$) + +

    +

    + <%= content_tag(:label, l(:dmsf_webdav_ignore_1b_file_for_authentication)) %> + <%= check_box_tag 'settings[dmsf_webdav_ignore_1b_file_for_authentication]', '1', + RedmineDmsf.dmsf_webdav_ignore_1b_file_for_authentication? %> + + <%= l(:dmsf_webdav_ignore_1b_file_for_authentication_info) %>
    + <%= l(:label_default) %>: <%= l(:general_text_Yes) %> +
    +

    +

    + <%= content_tag :label, l(:label_webdav_disable_versioning) %> + <%= text_field_tag 'settings[dmsf_webdav_disable_versioning]', RedmineDmsf.dmsf_webdav_disable_versioning, + size: 50 %> + + <%= l(:note_webdav_disable_versioning) %>
    + <%= l(:label_default) %>: ^\~\$|\.tmp$ +
    +

    +

    + <%= content_tag :label, l(:label_webdav_use_project_names) %> + <%= check_box_tag 'settings[dmsf_webdav_use_project_names]', '1', RedmineDmsf.dmsf_webdav_use_project_names? %> + + <%= l(:note_webdav_use_project_names) %>
    + <%= l(:label_default)%>: <%= l(:general_text_No) %> +
    +

    +
    +
    + +
    + + <%= l(:label_full_text) %> + + +<% if RedmineDmsf::Plugin.lib_available?('xapian') %> +

    + <%= content_tag :label, l(:label_index_database) %> + <%= text_field_tag 'settings[dmsf_index_database]', RedmineDmsf.dmsf_index_database, size: 50 %> + + <%= l(:label_default) %>: <%= File.expand_path('dmsf_index', Rails.root) %> + +

    + + <% stem_langs = %w(danish dutch english finnish french german hungarian italian norwegian portuguese romanian russian + spanish swedish turkish) %> + +

    + <%= content_tag :label, l(:label_stemming_language) %> + <%= select_tag 'settings[dmsf_stemming_lang]', options_for_select(stem_langs, RedmineDmsf.dmsf_stemming_lang) %> + + <%= l(:note_possible_values) %>: <%= stem_langs.join(', ') %>.
    + <%= "#{l(:label_default)}: #{stem_langs[2]}" %> +
    +

    + +

    + <%= content_tag :label, l(:label_stem_strategy) %> + <%= radio_button_tag 'settings[dmsf_stemming_strategy]', 'STEM_NONE', + RedmineDmsf.dmsf_stemming_strategy == 'STEM_NONE' %> <%= l(:option_stem_none) %> +
    + <%= radio_button_tag 'settings[dmsf_stemming_strategy]', 'STEM_SOME', + RedmineDmsf.dmsf_stemming_strategy == 'STEM_SOME' %> <%= l(:option_stem_some) %> +
    + <%= radio_button_tag 'settings[dmsf_stemming_strategy]', 'STEM_ALL', + RedmineDmsf.dmsf_stemming_strategy == 'STEM_ALL' %> <%= l(:option_stem_all) %> +
    + + <%= l(:text_stemming_info) %> + +

    + +

    + <%= content_tag :label, l(:label_enable_cjk_ngrams) %> + <%= check_box_tag 'settings[dmsf_enable_cjk_ngrams]', '1', RedmineDmsf.dmsf_enable_cjk_ngrams? %> + + <%= l(:text_enable_cjk_ngrams) %>
    + <%= l(:label_default)%>: <%= l(:general_text_No) %> +
    +

    +<% else %> +

    <%= l(:warning_xapian_not_available) %>

    +<% end %> diff --git a/assets/images/dmsf.png b/assets/images/dmsf.png new file mode 100644 index 00000000..0fbdbd04 Binary files /dev/null and b/assets/images/dmsf.png differ diff --git a/assets/javascripts/attachments_dmsf.js b/assets/javascripts/attachments_dmsf.js new file mode 100644 index 00000000..0e3fe97a --- /dev/null +++ b/assets/javascripts/attachments_dmsf.js @@ -0,0 +1,407 @@ +/* + Redmine plugin for Document Management System "Features" + + Karel Pičman + + 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 + . + */ + +function dmsfAddLink(linksSpan, linkId, linkName, title, project, awf) { + let attachmentsForm = linksSpan.closest('.dmsf-uploader') + let attachmentsIcons = attachmentsForm.find('.dmsf-attachments-icons'); + let delIcon = attachmentsIcons.find('svg.svg-del').clone(); + let linkIcon = attachmentsIcons.find('svg.svg-dmsf-link').clone(); + let assignmentIcon = attachmentsIcons.find('svg.svg-dmsf-assignment').clone(); + let nextLinkId = dmsfAddLink.nextLinkId++; + let linkSpan = $('', { id: 'dmsf_links_attachments_' + nextLinkId, 'class': 'attachment' }); + let iconDel = $('') + .attr({href: '#', class: 'remove-upload icon-only icon-del', title: 'Delete'}) + .append(delIcon); + let inputId = $('', {type: 'hidden', name: 'dmsf_links[' + nextLinkId + ']'}).val(linkId); + let inputName = $('', {type: 'text', class: 'filename icon icon-link readonly'}).val(linkName); + linkSpan.append(inputId); + linkSpan.append(linkIcon); + linkSpan.append(inputName); + linkSpan.append(iconDel.click(dmsfRemoveFileLbl)); + if(awf) { + let iconWf = $('').attr({href: "/dmsf_workflows/" + project + "/assign?dmsf_link_id=" + linkId, + 'class': 'modify-upload icon-only icon-ok', 'data-remote': 'true', 'title': title}).append(assignmentIcon); + linkSpan.append(iconWf); + } + linksSpan.append(linkSpan); +} + +dmsfAddLink.nextLinkId = 1000; + +/* Remove the extension and replace underscores with spaces, 'after_init.rb' -> 'after init' */ +function filenameToTitle(filename) { + return filename.replace(/\.[^/.]+$/, "").replace(/_+/g, " "); +} + +/* File size to human readable file size, 1024 -> 1.00 KB */ +function humanFileSize(bytes) { + var u = 0, s= 1024; + while (bytes >= s || -bytes >= s) { + bytes /= s; + u++; + } + return (u ? bytes.toFixed(2) + ' ' : bytes) + ' KMGTPEZY'[u] + 'B'; +} + +/* Increase version */ +function increaseVersion(version, max) { + let res; + if (version >= 0) { + if ((version + 1) < max) { + res = ++version; + } else { + res = version; + } + } else { + if (-(version - 1) < 90 /* 'Z' */) { + res = --version; + } else + res = version; + } + if (res < 0) { + res = String.fromCharCode(-res); // -65 => 'A' + } + return res; +} + +/* Get next version */ +function getNextVersion(filename, files) { + for(let i = 0; i < files.length; i++) { + if(filename === files[i][0]) { + if(files[i][3] && (files[i][3] >= 0)) { + return [files[i][1], files[i][2], increaseVersion(files[i][3], 1000)]; + } + if(files[i][2] && (files[i][2] >= 0)) { + return [files[i][1], increaseVersion(files[i][2], 1000), null]; + } + return [increaseVersion(files[i][1], 100), null, null]; + } + } + return [0, 1, null]; +} + +/* Get the current version */ +function getCurrentVersion(filename, files) { + for(let i = 0; i < files.length; i++) { + if (filename === files[i][0]) { + let res = ''; + if (files[i][3] != null) { + res = '.' + files[i][3]; + } + if (files[i][2] != null) { + res = '.' + files[i][2] + res; + } + if (files[i][1] != null) { + res = files[i][1] + res; + } + return res; + } + } + return '0.1.0'; +} + +/* Detects locked file */ +function isFileLocked(filename, files) { + for(let i = 0; i < files.length; i++) { + if (filename === files[i][0]) { + return files[i][4]; + } + } + return false; +} + +/* Replace selected version */ +function replaceVersion(detailsForm, attachmentId, name, version) { + let index = detailsForm.search('id="committed_files_' + attachmentId + '_version_' + name + '"'); + if (index != -1) { + let str = detailsForm.substring(index); + // Remove the original selection + str = str.replace('selected="selected" ', ''); + // Select new version + if (version != null) { + str = str.replace('', ''); + } + else { + str = str.replace('', ''); + } + detailsForm = detailsForm.substring(0, index) + str; + } + return detailsForm; +} + +function dmsfRevisionDetails(elem, attachmentId) { + let newRevisionForm = $('#dmsf_attachments_details_' + attachmentId); + newRevisionForm.toggle(); + elem.text("[" + (newRevisionForm.is(':visible') ? "-" : "+") + "]"); +} + +function dmsfAddFile(inputEl, file, eagerUpload) { + let attachments = $('#dmsf_attachments_fields'); + let max = ($(inputEl).attr('multiple') == 'multiple') ? 10 : 1 + let attachmentsForm = $(inputEl).closest('.dmsf-uploader') + let attachmentsIcons = attachmentsForm.find('.dmsf-attachments-icons'); + let delIcon = attachmentsIcons.find('svg.svg-del').clone(); + let attachmentIcon = attachmentsIcons.find('svg.svg-attachment').clone(); + let assignmentIcon = attachmentsIcons.find('svg.svg-dmsf-assignment').clone(); + if (attachments.children('.attachment').length < max) { + let attachmentId = dmsfAddFile.nextAttachmentId++; + let fileSpan = $('', { id: 'dmsf_attachments_' + attachmentId, 'class': 'attachment' }); + let iconDel = $('') + .attr({href: '#', class: 'remove-upload icon-only icon-del', title: 'Delete'}) + .append(delIcon) + .toggle(!eagerUpload); + let fileName = $('', {type: 'text', 'class': 'filename icon icon-attachment readonly', + name: 'dmsf_attachments[' + attachmentId + '][filename]', readonly: 'readonly'}).val(file.name); + fileSpan.append(attachmentIcon); + fileSpan.append(fileName); + if($(inputEl).attr('multiple') == 'multiple') { + fileSpan.append(iconDel.click(dmsfRemoveFileLbl)); + if ($(inputEl).data('awf')) { + let iconWf = $('').attr({ + href: '/dmsf_workflows/' + $(inputEl).attr( + 'data-project') + "/assign?attachment_id=" + attachmentId, + class: 'modify-upload icon-only icon-ok', + 'data-remote': 'true', + title: 'Assign an approval workflow' + }).append(assignmentIcon); + fileSpan.append(iconWf); + } + // Details + let detailsDiv = $('
    ').attr({id: 'dmsf_attachments_details_' + attachmentId}); + let detailsArrow = $(''); + detailsArrow.text('[+]'); + detailsArrow.attr({href: "#", 'data-cy': 'toggle__new_revision_from_content--dmsf', title: 'Details', + class: 'dmsf-plus-button'}); + detailsArrow.attr('onclick', "dmsfRevisionDetails($(this), " + attachmentId + "); return false;"); + let files = $(inputEl).data('files'); + let locked = isFileLocked(file.name, files); + let detailsForm = $(inputEl).data(locked ? 'dmsf-file-details-form-locked' : 'dmsf-file-details-form'); + // Index + detailsForm = detailsForm.replace(/\[0\]/g, '[' + attachmentId + ']'); + detailsForm = detailsForm.replace(/_0/g, '_' + attachmentId); + // Name + detailsForm = detailsForm.replace('id="committed_files_' + attachmentId + '_name" value=""', + 'id="committed_files_' + attachmentId + '_name" value="' + file.name + '"'); + // Title + detailsForm = detailsForm.replace('id="committed_files_' + attachmentId + '_title"', + 'id="committed_files_' + attachmentId + '_title" value = "' + filenameToTitle(file.name) + '"'); + // Size + detailsForm = detailsForm.replace('id="committed_files_' + attachmentId + '_human_size"', + 'id="committed_files_' + attachmentId + '_human_size" value = "' + humanFileSize(file.size) + '"'); + detailsForm = detailsForm.replace('id="committed_files_' + attachmentId + '_size" value="0"', + 'id="committed_files_' + attachmentId + '_size" value = "' + file.size + '"'); + // Mime type + detailsForm = detailsForm.replace('id="committed_files_' + attachmentId + '_mime_type"', + 'id="committed_files_' + attachmentId + '_mime_type" value = "' + file.type + '"'); + // Version + let version; + if(locked) { + version = getCurrentVersion(file.name, files); + detailsForm = detailsForm.replace('id="committed_files_' + attachmentId + '_version" value="0.0"', + 'id="committed_files_' + attachmentId + '_version" value="' + version + '"'); + } else { + version = getNextVersion(file.name, files); + detailsForm = replaceVersion(detailsForm, attachmentId, 'patch', version[2]); + detailsForm = replaceVersion(detailsForm, attachmentId, 'minor', version[1]); + detailsForm = replaceVersion(detailsForm, attachmentId, 'major', version[0]); + } + detailsDiv.append(detailsForm); + detailsDiv.hide(); + fileSpan.append(detailsArrow) + fileSpan.append(detailsDiv); + attachments.append(fileSpan); + } else { + fileSpan.append(iconDel.click(dmsfRemoveFileLbl)); + attachments.append(fileSpan); + $('#dmsf_file_revision_name').val(file.name); + } + if(eagerUpload) { + dmsfAjaxUpload(file, attachmentId, fileSpan, inputEl); + } + return attachmentId; + } + return null; +} + +dmsfAddFile.nextAttachmentId = 1; + +function dmsfAjaxUpload(file, attachmentId, fileSpan, inputEl) { + function onLoadstart(e) { + fileSpan.removeClass('ajax-waiting'); + fileSpan.addClass('ajax-loading'); + $('input:submit', $(this).parents('form')).attr('disabled', 'disabled'); + } + function onProgress(e) { + if(e.lengthComputable) { + this.progressbar('value', e.loaded * 100 / e.total); + } + } + function actualUpload(file, attachmentId, fileSpan, inputEl) { + dmsfAjaxUpload.uploading++; + dmsfUploadBlob(file, $(inputEl).data('upload-path'), attachmentId, { + loadstartEventHandler: onLoadstart.bind(progressSpan), + progressEventHandler: onProgress.bind(progressSpan) + }) + .done(function(result) { + progressSpan.progressbar('value', 100).remove(); + fileSpan.find('input.description, a').css('display', 'inline-block'); + }) + .fail(function(result) { + progressSpan.text(result.statusText); + }).always(function() { + dmsfAjaxUpload.uploading--; + fileSpan.removeClass('ajax-loading'); + let form = fileSpan.parents('form'); + if ((form.queue('upload').length == 0) && (dmsfAjaxUpload.uploading == 0)) { + $('input:submit', form).removeAttr('disabled'); + } + form.dequeue('upload'); + }); + } + let progressSpan = $('
    ').insertAfter(fileSpan.find('input.filename')); + progressSpan.progressbar(); + fileSpan.addClass('ajax-waiting'); + let maxSyncUpload = $(inputEl).data('max-concurrent-uploads'); + if(maxSyncUpload == null || maxSyncUpload <= 0 || dmsfAjaxUpload.uploading < maxSyncUpload) + actualUpload(file, attachmentId, fileSpan, inputEl); + else + $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl)); +} + +dmsfAjaxUpload.uploading = 0; + +function dmsfRemoveFileLbl() { + let span = $(this).parent('span'); + span.next('div').remove(); + span.next('br').remove(); + span.remove(); + return false; +} + +function dmsfUploadBlob(blob, uploadUrl, attachmentId, options) { + let actualOptions = $.extend({ + loadstartEventHandler: $.noop, + progressEventHandler: $.noop + }, options); + uploadUrl = uploadUrl + '?attachment_id=' + attachmentId; + if (blob instanceof window.File) { + uploadUrl += '&filename=' + encodeURIComponent(blob.name); + uploadUrl += '&content_type=' + encodeURIComponent(blob.type); + } + return $.ajax(uploadUrl, { + type: 'POST', + contentType: 'application/octet-stream', + beforeSend: function(jqXhr, settings) { + jqXhr.setRequestHeader('Accept', 'application/js'); + // attach proper File object + settings.data = blob; + }, + xhr: function() { + let xhr = $.ajaxSettings.xhr(); + xhr.upload.onloadstart = actualOptions.loadstartEventHandler; + xhr.upload.onprogress = actualOptions.progressEventHandler; + return xhr; + }, + data: blob, + cache: false, + processData: false + }); +} + +function dmsfAddInputFiles(inputEl) { + let clearedFileInput = $(inputEl).clone().val(''); + let addFileSpan = $('.dmsf_add_attachment'); + if ($.ajaxSettings.xhr().upload && inputEl.files) { + // upload files using ajax + dmsfUploadAndAttachFiles(inputEl.files, inputEl); + $(inputEl).remove(); + } else { + // browser not supporting the file API, upload on form submission + let attachmentId; + let aFilename = inputEl.value.split(/\/|\\/); + attachmentId = dmsfAddFile(inputEl, {name: aFilename[aFilename.length - 1]}, false); + if (attachmentId) { + $(inputEl).attr({name: 'dmsf_attachments[' + attachmentId + '][file]', style: 'display:none;'}).appendTo( + '#dmsf_attachments_' + attachmentId); + } + } + clearedFileInput.val(''); + addFileSpan.prepend(clearedFileInput); +} + +function dmsfUploadAndAttachFiles(files, inputEl) { + let maxFileSize = $(inputEl).data('max-file-size'); + let maxFileSizeExceeded = $(inputEl).data('max-file-size-message'); + let sizeExceeded = false; + let filesLength = $('#dmsf_attachments_fields').children().length + files.length + $.each(files, function() { + if (this.size && maxFileSize != null && this.size > parseInt(maxFileSize)) { + sizeExceeded = true; + } + }); + if (sizeExceeded) { + window.alert(maxFileSizeExceeded); + } else { + $.each(files, function() { + dmsfAddFile(inputEl, this, true); + }); + } + if (filesLength > ($(inputEl).attr('multiple') == 'multiple' ? 10 : 1)) { + window.alert($(inputEl).data('max-number-of-files-message')); + } +} + +function dmsfHandleFileDropEvent(e) { + $(this).removeClass('fileover'); + blockEventPropagation(e); + if ($.inArray('Files', e.dataTransfer.types) > -1) { + dmsfUploadAndAttachFiles(e.dataTransfer.files, $('input:file.file_selector')); + } +} + +function dmsfDragOverHandler(e) { + $(this).addClass('fileover'); + blockEventPropagation(e); +} + +function dmsfDragOutHandler(e) { + $(this).removeClass('fileover'); + blockEventPropagation(e); +} + +function dmsfSetupFileDrop() { + if (window.File && window.FileList && window.ProgressEvent && window.FormData) { + if($().jquery < '3.0.0') { + $.event.fixHooks.drop = {props: ['dataTransfer']}; + } + else{ + $.event.addProp('dataTransfer'); + } + $('form span.dmsf-uploader:not(.dmsffiledroplistner)').has('input:file').each(function () { + + $(this).on({ + dragover: dmsfDragOverHandler, + dragleave: dmsfDragOutHandler, + drop: dmsfHandleFileDropEvent + }).addClass('dmsffiledroplistner'); + }); + } +} + +$(document).ready(dmsfSetupFileDrop); diff --git a/assets/javascripts/dmsf_button.js b/assets/javascripts/dmsf_button.js new file mode 100644 index 00000000..5f4ac527 --- /dev/null +++ b/assets/javascripts/dmsf_button.js @@ -0,0 +1,71 @@ +/* + Redmine plugin for Document Management System "Features" + + Karel Pičman + + 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 + . +*/ + +/* Global jsToolBar */ + +/* Space */ +jsToolBar.prototype.elements.dmsf_space = { + type: 'space' +} + +/* DMSF button */ +jsToolBar.prototype.elements.dmsf = { + type: 'button', + title: 'DMS', + fn: { + wiki: function() { + let This = this; + this.dmsfMenu(function(macro){ + This.encloseLineSelection('{{' + macro + '(', ')}}'); + }); + } + } +}; + +/* DMSF button's popup menu */ +jsToolBar.prototype.dmsfMenu = function(fn){ + let menu = $('
      '); + for (let i = 0; i < this.dmsfList.length; i++) { + let reg = new RegExp('^dmsf'); + let item = this.dmsfList[i]; + if(reg.test(item)) { + let macroItem = $('
      ').text(item); + $('
    • ').html(macroItem).appendTo(menu).mousedown(function () { + fn($(this).text()); + }); + } + else { + $('
    • ').html('
      ').appendTo(menu); + let macroItem = $('
      ').text(item.split(';')[1]); + $('
    • ').html(macroItem).appendTo(menu).mousedown(function () { + window.open('/dmsf/help/wiki_syntax','_blank', 'width=480,height=480'); + }); + } + } + $('body').append(menu); + menu.menu().width(150).position({ + my: 'left top', + at: 'left bottom', + of: this.toolNodes['dmsf'] + }); + $(document).on('mousedown', function() { + menu.remove(); + }); + return false; +}; diff --git a/assets/javascripts/lang/dmsf_button-en.js b/assets/javascripts/lang/dmsf_button-en.js new file mode 100644 index 00000000..ea0c5f19 --- /dev/null +++ b/assets/javascripts/lang/dmsf_button-en.js @@ -0,0 +1,21 @@ +/* + Redmine plugin for Document Management System "Features" + + Karel Pičman + + 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 + . +*/ + +jsToolBar.strings = jsToolBar.strings || {}; +jsToolBar.strings['DMS'] = 'DMS'; diff --git a/assets/javascripts/redmine_dmsf.js b/assets/javascripts/redmine_dmsf.js new file mode 100644 index 00000000..36a1dd56 --- /dev/null +++ b/assets/javascripts/redmine_dmsf.js @@ -0,0 +1,157 @@ +/* + Redmine plugin for Document Management System "Features" + + Karel Pičman + + 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 + . +*/ + +/* Function to allow the projects to show up as a tree */ +function dmsfToggle(el, project_id, folder_id, url) +{ + // Expand not yet loaded selected row + let selectedRow = $(el).parents('tr').first(); + let expand = $(selectedRow).hasClass('dmsf-collapsed'); + + if(selectedRow.hasClass('dmsf-child')){ + + return; + } + + if(selectedRow.hasClass('dmsf-not-loaded')){ + + dmsfExpandRows(project_id, folder_id, selectedRow, url); + } + + let span = selectedRow.find('span.dmsf-expander'); + + if(expand) { + selectedRow.switchClass('dmsf-collapsed', 'dmsf-expanded'); + span.addClass('open'); + } + else { + selectedRow.switchClass('dmsf-expanded', 'dmsf-collapsed'); + span.removeClass('open'); + } + + // Hide collapsed rows and reset odd/even rows background colour + let oddeventoggle = 0; + + $("tr.dmsf-tree").each(function(i, tr){ + + // Visiblity + if($(tr).hasClass(folder_id ? (folder_id + 'f') : (project_id + 'p'))) { + if (expand) { + + // Display only children with expanded parent + let m = $(tr).attr('class').match(/(\d+(p|f)) idnt/); + + if(m){ + + if($("#" + m[1] + "span").hasClass('dmsf-expanded')){ + + $(tr).removeClass('dmsf-hidden'); + } + } + + } else { + + if(!$(tr).hasClass('dmsf-hidden')) { + $(tr).addClass('dmsf-hidden'); + } + } + } + + // Background + $(tr).removeClass('even'); + $(tr).removeClass('odd'); + + if (oddeventoggle === 0) { + + $(tr).addClass('odd'); + } + else { + + $(tr).addClass('even'); + } + + oddeventoggle ^= 1; + }); +} + +/* Add child rows */ +function dmsfExpandRows(project_id, folder_id, parentRow, url) { + + $(parentRow).removeClass('dmsf-not-loaded'); + + let idnt = 0; + let classes = ''; + let m = $(parentRow).attr('class').match(/idnt-(\d+)/); + + if(m){ + idnt = m[1]; + } + + m = $(parentRow).attr('class').match(/((\d|p|f|\s)+) idnt/); + + if(m){ + classes = m[1] + } + + m = $(parentRow).attr('id').match(/^(\d+(p|f))/); + + if(m){ + classes = classes + ' ' + m[1] + } + + $.ajax({ + url: url, + type: 'post', + dataType: 'html', + data: { + project_id: project_id, + folder_id: folder_id, + row_id: $(parentRow).attr('id'), + idnt: idnt, + classes: classes + } + }).done(function(data) { + // Hide the expanding icon if there are no children + if( m && (data.indexOf(' ' + m[1] + ' ') < 0)) { + + $(parentRow).removeClass('dmsf-expanded'); + $(parentRow).find('div.dmsf-row-control').removeClass('row-control dmsf-row-control'); + + if(!$(parentRow).hasClass('dmsf-child')) { + + $(parentRow).addClass('dmsf-child'); + } + } + else { + // Add child rows + return Function('"use strict";' + data)(); + } + }) + .fail(function() { + console.log('An error in rows expanding'); + }); +} + +function noteMandatory(mandatory) { + let note = $('textarea#note'); + note.prop('required', mandatory); + if(mandatory){ + note.focus(); + } +} diff --git a/assets/javascripts/select2.min.js b/assets/javascripts/select2.min.js new file mode 100644 index 00000000..e4214264 --- /dev/null +++ b/assets/javascripts/select2.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(u){var e=function(){if(u&&u.fn&&u.fn.select2&&u.fn.select2.amd)var e=u.fn.select2.amd;var t,n,r,h,o,s,f,g,m,v,y,_,i,a,b;function w(e,t){return i.call(e,t)}function l(e,t){var n,r,i,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&b.test(e[s])&&(e[s]=e[s].replace(b,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},i.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},i.__cache={};var n=0;return i.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},i.StoreData=function(e,t,n){var r=i.GetUniqueElementId(e);i.__cache[r]||(i.__cache[r]={}),i.__cache[r][t]=n},i.GetData=function(e,t){var n=i.GetUniqueElementId(e);return t?i.__cache[n]&&null!=i.__cache[n][t]?i.__cache[n][t]:o(e).data(t):i.__cache[n]},i.RemoveData=function(e){var t=i.GetUniqueElementId(e);null!=i.__cache[t]&&delete i.__cache[t],e.removeAttribute("data-select2-id")},i}),e.define("select2/results",["jquery","./utils"],function(h,f){function r(e,t,n){this.$element=e,this.data=n,this.options=t,r.__super__.constructor.call(this)}return f.Extend(r,f.Observable),r.prototype.render=function(){var e=h('
        ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},r.prototype.clear=function(){this.$results.empty()},r.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h(''),r=this.options.get("translations").get(e.message);n.append(t(r(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},r.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},r.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},r.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var i=t.eq(r);i.trigger("mouseenter");var o=l.$results.offset().top,s=i.offset().top,a=l.$results.scrollTop()+(s-o);0===r?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger("mouseenter");var i=l.$results.offset().top+l.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=l.$results.scrollTop()+o-i;0===n?l.$results.scrollTop(0):ithis.$results.outerHeight()||o<0)&&this.$results.scrollTop(i)}},r.prototype.template=function(e,t){var n=this.options.get("templateResult"),r=this.options.get("escapeMarkup"),i=n(e,t);null==i?t.style.display="none":"string"==typeof i?t.innerHTML=r(i):h(t).append(i)},r}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,r,i){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return r.Extend(o,r.Observable),o.prototype.render=function(){var e=n('');return this._tabindex=0,null!=r.GetData(this.$element[0],"old-tabindex")?this._tabindex=r.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,r=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",r),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&r.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o.prototype.isEnabled=function(){return!this.isDisabled()},o.prototype.isDisabled=function(){return this.options.get("disabled")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html(''),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var r=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",r).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",r),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),r=this.display(t,n);n.empty().append(r);var i=t.title||t.text;i?n.attr("title",i):n.removeAttr("title")}else this.clear()},i}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('
          '),e},n.prototype.bind=function(e,t){var r=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){r.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!r.isDisabled()){var t=i(this).parent(),n=l.GetData(t[0],"data");r.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return i('
        • ×
        • ')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n×');a.StoreData(r[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(r)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(r,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=r('');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),t.on("open",function(){r.$search.attr("aria-controls",i),r.$search.trigger("focus")}),t.on("close",function(){r.$search.val(""),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.trigger("focus")}),t.on("enable",function(){r.$search.prop("disabled",!1),r._transferTabIndex()}),t.on("disable",function(){r.$search.prop("disabled",!0)}),t.on("focus",function(e){r.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){r.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){r._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===r.$search.val()){var t=r.$searchContainer.prev(".select2-selection__choice");if(0this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(){r._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var r=this;this._checkIfMaximumSelected(function(){e.call(r,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var r=this;this.current(function(e){var t=null!=e?e.length:0;0=r.maximumSelectionLength?r.trigger("results:message",{message:"maximumSelected",args:{maximum:r.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){r.handleSearch(e)}),t.on("open",function(){r.$search.attr("tabindex",0),r.$search.attr("aria-controls",i),r.$search.trigger("focus"),window.setTimeout(function(){r.$search.trigger("focus")},0)}),t.on("close",function(){r.$search.attr("tabindex",-1),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.val(""),r.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||r.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(r.showSearch(e)?r.$searchContainer.removeClass("select2-search--hide"):r.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;0<=r;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("query",function(e){r.lastParams=e,r.loading=!0}),t.on("query:append",function(e){r.lastParams=e,r.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
        • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("open",function(){r._showDropdown(),r._attachPositioningHandler(t),r._bindContainerResultHandlers(t)}),t.on("close",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f(""),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,r="scroll.select2."+t.id,i="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(r,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(r+" "+i+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,r="resize.select2."+t.id,i="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+r+" "+i)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),r=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=i.top,o.bottom=i.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ai.bottom+s,d={left:i.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(r="below"),u||!c||t?!c&&u&&t&&(r="below"):r="above",("above"==r||t&&"below"!==r)&&(d.top=o.top-h.top-s),null!=r&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+r),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+r)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,r){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,r=0;r');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("jquery-mousewheel",["jquery"],function(e){return e}),e.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,o,t,s){if(null==i.fn.select2){var a=["open","close","destroy"];i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new o(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,r=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=s.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,r)}),-1, Karel Pičman + + 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 + . +*/ + +div#contents { + background-color: #F2F2F2; +} + +div.box { + border: solid 1px #FDBF3B; + background-color: #FFEBC1; + padding-left: 12px; + padding-right: 12px; + margin-bottom: 12px; + max-width: 90%; +} + +img { + max-width: 85%; +} + +p { + text-align: justify; + text-justify: inter-word; +} + +table { + border: 1px solid #58585A; + border-collapse:collapse; +} + +table th { + background-color: #A1BBD0; + font-weight: bold; + border: 1px solid #58585A; + border-collapse:collapse; +} + +table td { + background-color: #F2F2F2; + border: 1px solid #58585A; + border-collapse:collapse; + padding: 4px; +} + +table td.table-nowrap-column { + white-space: nowrap; +} + +ul.no-bullets { + list-style-type:none; +} \ No newline at end of file diff --git a/assets/stylesheets/redmine_dmsf.css b/assets/stylesheets/redmine_dmsf.css new file mode 100644 index 00000000..051201d2 --- /dev/null +++ b/assets/stylesheets/redmine_dmsf.css @@ -0,0 +1,362 @@ +/* + Redmine plugin for Document Management System "Features" + + Vit Jonas , Karel Pičman + + 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 + . +*/ + +/* Main view */ +.list .dmsf-title { + width: 40%; + text-align: left; +} + +.list .dmsf-buttons { + min-width: 18px; +} + +a.dmsf-label { + margin-left: 4px; +} + +/* Revision's downloads box */ +#dmsf_buttons.dmsf-controls { + float: left +} + +.dmsf-uploader span[id*="dmsf_"] .filename { + -webkit-box-shadow: none !important; + box-shadow: none !important; +} + +.dmsf-filename { + padding: 0 10px 0 10px; + float: right; + font-size: 0.8em; + white-space: nowrap; +} + +/* Approval workflow */ +#dmsf_users_for_delegate { + height: 200px; + overflow: auto; + column-width: auto +} + +#dmsf_users_for_delegate label { + display: unset; +} + +.dmsf-workflows.locked a { + color: #aaa; +} + +div[id^="step-index-"] { + display: none; +} + +/* DMSF revision box */ +h2.dmsf-header { + border: none !important; +} + +#new_revision_form_content { + display: none; +} + +form#new_revision_form { + margin: 0; +} + +.dmsf-revision-box { + background-color: #f6f6f6; + margin-bottom: 16px; +} + +.dmsf-revision-inner-box { + border: 1px solid #e4e4e4; + padding: 10px; + border-radius: 3px; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); +} + +div.dmsf-revision-inner-box .attribute { + padding: 0; + clear: left; + min-height: 1.8em; + border: none; +} + +div.dmsf-revision-inner-box .attribute .label { + margin-left: 0 !important; +} + +div.dmsf-revision-inner-box .attribute .label { + width: 170px; + margin-left: -180px; + font-weight: bold; + float: left; +} + +div.dmsf-id-box { + float: right; + white-space: nowrap; + line-height: 24px; + color: #505050; + margin-top: 5px; + padding-left: 10px; +} + +div#dmsf_new_revision { + padding: 8px; + margin: 0px 0px 12px 0px; + background-color: rgb(249.3, 251.9, 255); + color: #505050; + line-height: 1.5em; + border: 1px solid #d0d7de; + word-wrap: break-word; + border-radius: 3px; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); +} + +.dmsf-log-header-box{ + padding: 6px; + margin-bottom: 10px; +} +.dmsf-log-header-left { + width: 50%; + float: left; +} + +.dmsf-log-header-box label { + font-weight: bold; + margin-left: 0; + margin-right: 3px; + padding: 3px 0 3px 0; +} + +.dmsf-widget-header { + font-weight: normal; + padding: 0 10px 0 10px; + background: #e9e9e9; +} + +.dmsf-widget-header-text { + padding: 5px 0 0 0; +} + +div[id*="revision_access_"] { + display: none; +} + +.dmsf-description { + max-width: 100%; +} + +/* Links */ +.dmsf-gray, +.dmsf-gray a, +.dmsf-gray a:link, +.dmsf-gray a:visited { + color: gray; +} + +.dmsf-gray svg { + stroke: grey; +} + +svg.dmsf-gray { + stroke: grey; +} + +/* System folders */ +.dmsf-system, +.dmsf-system a, +.dmsf-system a:link, +.dmsf-system a:visited { + color: darkviolet; +} + +svg.dmsf-system { + stroke: darkviolet; +} + +/* DMSF tree view */ +.dmsf-hidden { display: none; } +.dmsf-tree:not(.dmsf-child) span.dmsf-expander { cursor: pointer; } +.dmsf-tree.dmsf-expanded span.dmsf-expander { + background: url("../../../images/arrow_down.png") no-repeat 0 50%; + padding-left: 16px; +} +.dmsf-tree.dmsf-child span.dmsf-expander { padding-left: 16px; } +.dmsf-tree.dmsf-collapsed span.dmsf-expander { + background: url("../../../images/arrow_right.png") no-repeat 0 50%; + padding-left: 16px; +} +.dmsf-tree.idnt-1 td.dmsf-title { padding-left: 1.5em; } +.dmsf-tree.idnt-2 td.dmsf-title { padding-left: 2em; } +.dmsf-tree.idnt-3 td.dmsf-title { padding-left: 2.5em; } +.dmsf-tree.idnt-4 td.dmsf-title { padding-left: 3em; } +.dmsf-tree.idnt-5 td.dmsf-title { padding-left: 3.5em; } +.dmsf-tree.idnt-6 td.dmsf-title { padding-left: 4em; } +.dmsf-tree.idnt-7 td.dmsf-title { padding-left: 4.5em; } +.dmsf-tree.idnt-8 td.dmsf-title { padding-left: 5em; } +.dmsf-tree.idnt-9 td.dmsf-title { padding-left: 5.5em; } + +.dmsf-select-version { + max-width: 50px; +} + +.dmsf-parent-container { + overflow: hidden; +} + +.dmsf-child-container { + float: left; + text-align: left; +} + +.dmsf-row-control { + float: left; +} + +/* DMSF file upload */ + +span.fileover { + background-color: lavender; +} + +.dmsf-uploader { + padding: 10px; + margin-bottom: 20px; + background-color: #f6f6f6; + color: #505050; + line-height: 1.5em; + border: 1px solid #e4e4e4; + word-wrap: break-word; + border-radius: 3px; + min-height: 50px; + display: block; + overflow: hidden; +} + +.dmsf-add-link { + display: block; + float: right; + margin-top: 10px; + margin-bottom: 10px; +} + +#dmsf_attachments_fields input.description { + margin-left: 4px; + width: 340px; +} + +#dmsf_attachments_fields span { + white-space: nowrap; +} + +#dmsf_attachments_fields input.filename { + border: 0; + height: 1.8em; + width: 250px; + color: #555; + background-color: inherit; +} + +.dmsf-plus-button { + vertical-align: middle; +} + +#dmsf_attachments_fields div.ui-progressbar { + width: 100px; + height: 14px; + margin: 2px 0 -5px 8px; + display: inline-block; +} + +#dmsf_links_attachments_fields span { + display: block; + white-space: nowrap; +} + +#dmsf_links_attachments_fields input.filename { + border: 0; + height: 1.8em; + width: 250px; + color: #555; + background-color: inherit; +} + +.attachments_fields .icon-link { + background-image: none; + padding-left: 0; +} + +.dmfs-box-tabular { + padding-top: 10px +} + +a.dmsf-scroll-down { + background-color: #759FCF; + text-decoration: none; + color: #FFFFFF; + font-weight: bold; + font-size: 0.8em; + float: right; + padding: 2px 9px 3px 20px; + margin-right: 20px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} + +div.dmsf-sticky { + position: fixed; + right: 10px; + top: 0; + z-index: 10000; +} + +div.dmsf-scroll { + width: 100%; +} + +/* New link form */ +#dmsf_link_target_file_id, #dmsf_link_target_folder_id, #dmsf_link_target_project_id, #dmsf_link_name { + min-width: 40%; +} + +#dmsf_link_external { + display: none; +} + +.dmsf_attachments_label { + vertical-align: middle; +} + +#dmsf_csv_export_options { + display: none; +} + +/* Email form */ +[id^='email_'].dmsf-full-width { + width: 90%; + max-width: 90%; +} + +/* Wiki toolbar */ +.jstb_dmsf { + background-image: url("/document.png"); +} diff --git a/assets/stylesheets/select2.min.css b/assets/stylesheets/select2.min.css new file mode 100644 index 00000000..7c18ad59 --- /dev/null +++ b/assets/stylesheets/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/config/locales/cs.yml b/config/locales/cs.yml new file mode 100644 index 00000000..783e3410 --- /dev/null +++ b/config/locales/cs.yml @@ -0,0 +1,499 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# +# 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 +# . + +cs: + dmsf: DMS # Custom fields tab title + label_dmsf_file: DMS Dokument + label_dmsf_file_plural: DMS Dokumenty # Email subject & Search options + label_dmsf_file_revision_plural: Revize dokumentů + label_dmsf_file_revision_access_plural: Přístupy k dokumentům + warning_no_entries_selected: Není nic vybráno + error_email_to_must_be_entered: Musí být zadán příjemce + warning_file_already_locked: Soubor je již zamčen + notice_file_locked: Soubor byl zamčen + warning_file_not_locked: Soubor není zamčen + notice_file_unlocked: Soubor byl odemčen + error_only_user_that_locked_file_can_unlock_it: Soubor může být odemčen pouze uživatelem, který ho zamkl + + error_max_files_exceeded: "Limit pro %{number} najednou stažených souborů je překročen" + error_entry_project_does_not_match_current_project: Zadaný projekt neodpovídá aktuálnímu projektu + + notice_folder_created: Složka byla vytvořena + error_folder_creation_failed: Vytváření složky selhalo + error_folder_title_must_be_entered: Musí být zadán název + notice_folder_deleted: Složka byla smazána + error_folder_title_is_already_used: Název již existuje + notice_folder_details_were_saved: Detaily složky byly uloženy + error_folder_is_locked: Složka je zamčena + error_file_is_locked: Soubor je zamčen + notice_file_deleted: Soubor byl smazán + error_at_least_one_revision_must_be_present: Musí existovat alespoň jedna revize + notice_revision_deleted: Revize byla smazána + notice_revision_obsoleted: Revize je zastaralá + warning_one_of_files_locked: Jeden ze souborů je zamčen + notice_file_revision_created: vytvořena nová revize + notice_your_preferences_were_saved: Vaše nastavení bylo uloženo + notice_your_preferences_were_not_saved: Your preferences were not saved + warning_folder_notifications_already_activated: Notifikace složky již byly aktivovány + + notice_folder_notifications_activated: Notifikace složky byly aktivovány + warning_folder_notifications_already_deactivated: Notifikace složky již byly deaktivovány + + notice_folder_notifications_deactivated: Notifikace složky byly deaktivovány + warning_file_notifications_already_activated: Notifikace souboru již byly aktivovány + + notice_file_notifications_activated: Notifikace souboru byly aktivovány + warning_file_notifications_already_deactivated: Notifikace souboru již byly deaktivovány + + notice_file_notifications_deactivated: Notifikace souboru byly deaktivovány + link_details: "%{title} detaily" + link_edit: "Upravit %{title}" + link_create_folder: Vytvořit složku + link_title: Název + link_size: Velikost + 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_restore_or_delete: Vybrat pro obnovení nebo smazání + + title_notifications_active_deactivate: "Notifikace aktivní: Deaktivovat" + title_notifications_not_active_activate: "Notifikace nejsou aktivní: Aktivovat" + title_title_version_version_download: "%{title} verze %{version} stáhnout" + title_locked_by_user: "Zamčeno uživatelem %{user}" + title_waiting_for_approval: Čeká na schválení + title_approved: Schváleno + 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 + link_user_preferences: Vaše nastavení + heading_send_documents_by_email: Odeslat dokumenty emailem + label_email_from: Od + label_email_to: Komu + label_email_cc: Kopie + label_email_subject: Předmět + label_email_documents: Documenty + label_email_body: Obsah + label_email_send: Odesláno + title_notifications_active: Notifikace jsou aktivní + label_upload_upload: Nahrát + heading_new_folder: Nová složka + label_title: Název + label_description: Popis + submit_save: Uložit + info_file_locked: Soubor je zamčen! + label_notifications: Notifikace + select_option_default: Výchozí + select_option_deactivated: Deaktivováno + select_option_activated: Aktivováno + label_title_format: Formát názvu + text_title_format: "Formát názvu souboru pro stažení (%t - název, %f - soubor, %d - datum, %v - verze, %i - ID, %r - + revize). Např.: %t_%v" + title_save_preferences: Uložit nastavení + heading_revisions: Revize + title_download: Stáhnout + title_delete_revision: Smazat revizi + title_obsolete_revision: Označit revizi jako zastaralou + label_created: Vytvořeno + label_changed: Změněno + info_changed_by_user: "%{changed} uživatelem" + label_filename: Jméno souboru + label_mime: Typ + label_size: Velikost + heading_new_revision: Nová revize + option_version_same: Stejná + option_version_patch: Revize + option_version_minor: Vedlejší + option_version_major: Hlavní + 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. + 0 znamená bez omezení. + label_file_storage_directory: Složka pro uložení souborů + label_index_database: Index databáze + label_stemming_language: "Jazyk pro 'Stemming'" + note_possible_values: Povolené hodnoty + note_pass_none_to_disable_stemming: "zadej 'nic' pro vypnutí 'Stemming'" + label_stem_strategy: "'Stem' strategie" + option_stem_none: Stem nic (výchozí) + option_stem_some: Stem něco + option_stem_all: Stem vše + text_stemming_info: "Tímto se řídí, jak parser dotazů použije stemovací algoritmus. Výchozí hodnota je STEM_NONE. + Možné hodnoty jsou: STEM_NONE - Neaplikuj žádný steming, STEM_SOME - Použij stemování pro výrazy kromě těch, které + začínají velkým písmenem nebo jsou následovány určitýmy znaky('/@<>=*[{\"') nebo jsou použité s operátory, které + vyžadují informaci o pozici. Stemované výrazy začínají písmenem 'Z', STEM_ALL - Hledej stemové výrazy ve všech + slovech (prefix 'Z' není přidán)" + 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_folders: Procházet dokumenty + permission_user_preferences: Nastavení uživatele + permission_view_dmsf_files: Zobrazit dokumenty + permission_folder_manipulation: Manipulace se složkami + permission_file_manipulation: Manipulace se soubory + permission_force_file_unlock: Vynucené odemknutí souboru + 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 + label_file: Soubor + field_folder: Složka + error_file_commit_require_uploaded_file: Potvrzení vyžaduje nahraný soubor + + warning_some_files_were_not_committed: "Některé soubory nebyly potvrzené z důvodu chyb při validaci: %{files}" + + error_user_has_not_right_delete_folder: Uživatel nemá právo mazat složky + + error_user_has_not_right_delete_file: Uživatel nemá právo mazat soubor + + notice_entries_deleted: Položky smazány + warning_some_entries_were_not_deleted: "Některé položky nebyly smazány: %{entries}" + title_delete_checked: Smaž vybrané + title_items: položek + title_filename_for_download: Název Zip archivu ke stažení + label_number_of_folders: Složky + label_number_of_documents: Dokumenty + error_file_storage_directory_does_not_exist: Cílová složka neexistuje a nemůže být vytvořena + + error_file_can_not_be_created: Nelze vytvořit soubor v cílové složce + error_wrong_zip_encoding: Chybné kódování Zipu + warning_xapian_not_available: Xapian není k dostupný + 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_first: První + heading_access_last: Poslední + label_dmsf_updated: Změněno + label_dmsf_downloaded: Staženo + title_total_size_of_all_files: Celková velikost všech souborů ve složce + project_module_dmsf: DMS # Project module name + warning_no_project_to_copy_file_to: Neexistuje projekt, do kterého můžete kopírovat + comment_copied_from: "Zkopírováno z %{source}" + field_target_project: Cílový projekt + field_target_folder: Cílová složka + title_copy_or_move: Kopírovat/Přesunout + label_dmsf_folder_plural: DMS Složky # Search options + comment_moved_from: "Přesunuto z %{source}" + 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. + (%{number} MB)" + note_maximum_email_filesize: Omezí se maximální velikost souboru, který může být poslán emailem. 0 znamená neomezený. + Číslo je v MB. + label_maximum_email_filesize: Maximální velikost souboru emailu + header_minimum_filesize: Chyba souboru. + error_minimum_filesize: "Soubor %{file} má nulovou velikost a nebude přiložen." + parent_directory: Nadřazená složka + note_webdav: "Webdav je po aktivaci k dispozici na %{protocol}://%{domain}/dmsf/webdav/[identifikátor projektu]" + + label_copy_dmsf: "Kopíruj dokumenty a složky (%{files} souborů v %{folders} složkách)" + label_copy_only_dmsf_folders: "Kopíruj jenom a složky (%{folders})" + + warning_folder_already_locked: Tato složka je již zamčená + notice_folder_locked: Složka byla úspěšně zamčena + warning_folder_not_locked: Složku nelze zamknout + notice_folder_unlocked: Složka byla odemčena + error_only_user_that_locked_folder_can_unlock_it: Nemáte oprávnění k odemknutí této složky + + title_unlock_folder: Odemknout + title_lock_folder: Zamknout + + select_option_webdav_readonly: Pouze pro čtení + select_option_webdav_readwrite: Čtení/Zápis + label_webdav_strategy: Webdav strategie + + note_webdav_strategy: Umožní administrátorovi rozhodnout, zdali je webdav pouze pro čtení nebo i pro zápis. + + + error_unable_delete_dmsf_workflow: Nelze smazat schvalovací proces + error_empty_note: Musí být vyplněn komentář + error_workflow_assign: Chyba při přiřazování + error_cannot_start_workflow: Schvalovací proces nemůže být zahájen + error_cannot_renumber_steps: Schvalovací kroky nelze přečíslovat + label_dmsf_workflow_new: Nový schvalovací proces + field_label_dmsf_workflow: Schvalovací proces + field_label_dmsf_workflow_name: Approval workflow name + label_dmsf_workflow_plural: Schvalovací procesy + label_dmsf_workflow_plural_num: Kopírovat schvalovací procesy (%{count}) + label_dmsf_workflow_step: Krok + label_dmsf_workflow_step_plural: Kroky + label_dmsf_workflow_approval_plural: Schválení + label_dmsf_wokflow_action_approve: Schválit + label_dmsf_wokflow_action_reject: Odmítnout + label_dmsf_wokflow_action_delegate: Delegovat na + label_dmsf_wokflow_action_assign: Přiřadit schvalovací proces + label_dmsf_wokflow_action_start: Zahájit schvalovací proces + label_dmsf_workflow_add_approver: "Přidat schvalovatele s logickou funkcí:" + label_or: nebo + label_action: Akce + label_note: Komentář + title_none: Žádný + title_rejection: Zamítnutí + title_delegation: Delegace + title_assignment: Přiřazení + title_start: Zahájení + title_dmsf_workflow_log: Historie schvalovacího procesu + title_assigned: Přiřazený + title_approval: Schválený + title_rejected: Zamítnutý + title_obsolete: Zastaralá + dmsf_and: A + dmsf_or: NEBO + dmsf_new_step: Nový krok + dmsf_new_step_or_approver: Nový krok nebo Nový schvalovatel + message_dmsf_wokflow_note: Váš komentář... + info_revision: "r %{rev}" + link_workflow: Schvalovací proces + notice_workflow_started: Schvalovací proces byl úspěšně zahájen + text_email_subject_approved: schválen + text_email_subject_rejected: zamítnut + text_email_subject_delegated: delegován + text_email_subject_requires_approval: očekává Vaše schválení + text_email_subject_updated: aktualizován + text_email_subject_started: spuštěn + text_email_finished_approved: "Schvalovací proces '%{name}' přiřazený k dokumentu '%{filename}' byl právě ukončen a + dokument je schválen." + text_email_finished_rejected: "Schvalovací proces '%{name}' přiřazený k dokumentu '%{filename}' byl dokončen a + dokument byl zamítnut, protože '%{notice}'." + text_email_finished_delegated: "Schvalovací proces '%{name}' přiřazený k dokumentu '%{filename}' byl delegován, + protože '%{notice}' a od Vás se očekává schválení v aktuálním schvalovacím kroku '%{stepname}'." + text_email_finished_step: "Schvalovací proces '%{name}' přiřazený k dokumentu '%{filename}' právě ukončil jeden ze + schvalovacích kroků a od Vás se očekává schválení v dalším schvalovacím kroku." + text_email_finished_step_short: "Schvalovací proces '%{name}' přiřazený k dokumentu '%{filename}' právě ukončil jeden + ze schvalovacích kroků." + text_email_started: "Schvalovací proces '%{name}' přiřazený k dokumentu '%{filename}' byl zahájen a od Vás se očekává + schválení v aktuálním schvalovacím kroku '%{stepname}'." + text_email_to_proceed: Pro schválení klikněte na zaškrtávací ikonku vedle dokumentu v + text_email_to_see_history: Pro zobrazení historie schvalovacího procesu klikněte na status dokumentu v + + text_email_to_see_status: Pro zobrazení aktuálního stavu schvalovacího procesu klikněte na status dokumentu v + + + title_create_link: Vytvořit symbolický odkaz + label_link_from: Odkaz z + label_link_to: Odkaz do + label_notifications_on: Zapnout notifikace + label_notifications_off: Vypnout notifikace + field_target_file: Zdrojový soubor + title_download_entries: Historie stahování + label_external: Externí + label_internal: Interní + + label_link_name: Název odkazu + field_external_url: URL + label_target_folder: Cílová složka + label_source_folder: Zdrojová složka + label_target_project: Cílový projekt + label_source_project: Zdrojový projekt + + text_email_doc_updated_subject: Dokumenty aktualizovány + text_email_doc_updated: právě aktualizoval dokumenty projektu + text_email_doc_follows: takto + text_email_doc_deleted_subject: Dokumenty smazány + text_email_doc_deleted: právě smazal dokumety projektu + 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. + + warning_email_notifications: "Notifikační email poslán na uživatele %{to}" + + link_trash_bin: Koš + title_restore: Obnovit + notice_dmsf_file_restored: Document byl úspěšně obnoven + notice_dmsf_folder_restored: Složka byla úspěšně obnovena + notice_dmsf_link_restored: Odkaz byl úspěšně obnoven + title_restore_checked: Obnov vybrané + error_parent_folder: Nadřazená složka neexistuje + + error_resource_or_parent_locked: Nelze zamknout - zdrojový nebo nadřazený objekt je zamčený + error_parent_locked: Nelze zamknout - nadřazený objekt je zamčený + error_resource_locked: Nelze zamknout - zdrojový objekt je zamčený + error_lock_exclusively: Nelze zamknout již zamčený objekt + error_unlock_parent_locked: Nelze odemknout - nadřazený objekt je zamčený + + label_dmsf_version: Verze + + locked_documents: Zamčené dokumenty + open_approvals: Procesy ke schválení + watched_documents: Sledované dokumenty + + error_maximum_upload_filecount: "Nelze nahrát více než %{filecount} soubor(ů)." + + label_public_urls: Veřejné URL platné do + + label_webdav: WebDAV + label_full_text: Full-textové vyhledávání + link_extension: Příp. + + label_webdav_ignore: Vzory názvů ignorovaných souborů + note_webdav_ignore: Regulární výraz pro názvy souborů, které budou ignorovány při volání PUT. + + label_document_url: Url + label_last_revision_id: Revize + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. The default pattern + matches temporary files created by MsOffice. + + label_dmsf_keep_documents_locked: Ponechat dokumenty zamčené + note_dmsf_keep_documents_locked: Dokumenty zůstanou i po schválení zamčené. + note_global: (globální) + field_dmsf_not_inheritable: Není dědičné + + label_webdav_use_project_names: Použít názvy projektů + note_webdav_use_project_names: Použít názvy projektů místo identifikátorů pro názvy složek. + + label_last_approver: Poslední schvalovatel + + label_act_as_attachable: Jako příloha + note_dmsf_act_as_attachable: Umožní přikládat dokumenty k objektům např. k úkolům. + + label_user_search_add: Vyhledej uživatele pro přidání + + label_dmsf_attachments: DMS přílohy + label_basic_attachments: Souborové přílohy + + label_email_from_override: Od + text_email_from_override: aktuálně přihlášený uživatel + label_email_reply_to: Odpovědět komu + + 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" + + 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í + procesu vytváření odkazů nebo kopírování dokumentů a složek. + + label_dmsf_permissions: Povolený přístup pro + label_inherited_permissions: Zděděný přístup pro + + button_edit_content: Upravit obsah + field_workflow: Workflow + field_modified: Datum + field_updated: Datum + field_count: Stažení + field_first_at: První + field_last_at: Poslední + field_size: Velikost + field_locked: Zamčen + + label_add_width: Přidat s + + dmsf_webdav_ignore_1b_file_for_authentication: Ignorovat 1b soubor poslaný kvůli autentizaci + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: Složka není prázdná. + label_scroll_down: Posunout se dolů + note_webdav_disabled: WebDAV je zablokovaný. Kontaktujte administrátora. + + dmsf_copy: "Kopie (%{n})" + label_empty_trash_bin: Vysypat koš + label_dmsf_projects_as_subfolders: Podprojekty jako podaresáře + note_dmsf_projects_as_subfolders: Přidá podprojekty jako podsložky do pohledu DMS + only_approval_zero_minor_version: Schvalovat pouze hlavní verze + title_assignment_minor: Přiřazení není dovoleno, podružná verze musí být nula + title_start_minor: Zahájení není dovoleno, podružná verze musí být nula + title_approval_minor: Schválení není dovoleno, podružná verze musí být nula + + label_project_watchers: Sledující uživatelé + label_dmsf_folder_watchers: Sledující uživatelé + label_dmsf_file_watchers: Sledující uživatelé + label_dmsf_watched: Sledované dokumenty + dmsf_legacy_notifications: Původní DMS notifikace + permission_view_dmsf_folder_watchers: Zobrazit sledující složky + permission_add_dmsf_folder_watchers: Přidat sledujícího složky + permission_delete_dmsf_folder_watchers: Smazat sledující složky + permission_view_dmsf_file_watchers: Zobrazit sledující dokumentu + permission_add_dmsf_file_watchers: Přidat sledujícího dokumentu + permission_delete_dmsf_file_watchers: Smazat sledující dokumentu + permission_view_project_watchers: Zobrazit sledující projektu + permission_add_project_watchers: Přidat sledujícího projektu + permission_delete_project_watchers: Smazat sledující projektu + label_dmsf_new_top_level_document: Nový kořenový DMS dokument + label_dmsf_new_top_level_folder: Nová kořenová DMS složka + + label_dmsf_max_notification_receivers_info: Maximální počet zobrazených příjemců notifikace + note_dmsf_max_notification_receivers_info: Omezí maximální počet zobrazených příjemců e-mailové notifikace + label_dmsf_office_bin: Binárka Libreofficu + note_dmsf_office_bin: Binárka ke konverzi kancelářských dokumentů do formátu PDF k poskytnutí jejich náhledu. Jestliže + náhledy nechcete, vložte sem prázdný řetězec. Při změně budete muset, pravděpodobně, + restartovat aplikaci. + note_dmsf_office_bin_not_available: "Příkaz LibreOfficu pro příkazovou řádku '%{value}' není k dispozici" + + label_dmsf_columns: DMS sloupce + label_column_id: ID + label_column_title: Název + label_column_size: Velikost + label_column_modified: Změněno + label_column_version: Verze + label_column_workflow: Schvalování + label_column_author: Autor + label_column_description: Popis + label_column_comment: Komentář + + label_dmsf_global_menu_disabled: Globální DMS menu zakázáno + note_dmsf_global_menu_disabled: Pokud je zašrtnuto, tak položka DMS není v hlavním menu. + error_dmsf_workflow_assigned: Použitý schvalovací proces nemůže být ani upraven ani smazán. + + label_empty_minor_version_by_default: Prázdná vedlejší verze jako výchozí + text_email_doc_downloaded_subject: Stažení dokumentů + text_email_doc_downloaded: právě stáhl dokumety projektu + field_default_dmsf_query: Výchozí dotaz DMS + field_receive_download_notification: Dostávat notifikace o stažení + + label_remove_original_documents_module: Odstranit původní modul Dokumenty + + notice_entries_copied: Kopírování se podařilo + notice_entries_moved: Přesun se podařil + label_dmsf_file_revision: DMS Dokument rev. + error_not_supported_image_format: Nepodporovaný formát obrázku + error_not_supported_video_format: Nepodporovaný formát videa + + label_webdav_authentication: WebDAV autentifikace + note_webdav_authentication: Autentifikační metoda Basic není považována za bezpečnou a z toho důvodu je blokována + některými klienty. Metoda Digest je založena na automaticky vygenerovaném digestu. Uživatelé se pak z WebDAV klienta + autentifikují také pomocí uživatelského jména a hesla. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Chybějící DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: Je vyžadováno vaše heslo pro vygenerování nového DMS WebDAV digestu. + notice_webdav_digest_reset: Váš DMS WebDAV digest byl resetován. + + label_dmsf_commit: Potvrdit + label_dmsf_upload_commit: Nahrát a potvrdit + + notice_search_in_subfolders: Vyhledávání v podsložkách není rekurzivní. Pro rekurzivní vyhledávání běžte do nejvyšší úrovně. + warning_folder_unlockable: Složku nelze odemknout + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: obsahuje neplatné znaky \ No newline at end of file diff --git a/config/locales/de.yml b/config/locales/de.yml new file mode 100644 index 00000000..90c13c14 --- /dev/null +++ b/config/locales/de.yml @@ -0,0 +1,495 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Terrence Miller, Christian Wetting , Karel Pičman +# +# 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 +# . + +de: + dmsf: DMS # Custom fields tab title + label_dmsf_file: DMS Dokument + label_dmsf_file_plural: DMS Dokumente # E-Mail subject & Search options + label_dmsf_file_revision_plural: Dokumentenversion + label_dmsf_file_revision_access_plural: Dokumentenzugriffe + warning_no_entries_selected: Keine Einträge ausgewählt + error_email_to_must_be_entered: Es muss ein E-Mail-Empfänger angegeben werden. + warning_file_already_locked: Datei schon gesperrt + notice_file_locked: Datei gesperrt + warning_file_not_locked: Datei nicht gesperrt + notice_file_unlocked: Datei freigegeben + error_only_user_that_locked_file_can_unlock_it: Nur der Benutzer, der die Datei gesperrt hat, kann sie auch wieder freigeben + + error_max_files_exceeded: "Grenze für %{number} gleichzeitig heruntergeladene Dateien überschritten" + error_entry_project_does_not_match_current_project: Eingangsprojekt entspricht nicht aktuellem Projekt + + notice_folder_created: Ordner erstellt + error_folder_creation_failed: Ordnererstellung fehlgeschlagen + error_folder_title_must_be_entered: Es muss ein Titel angegeben werden + notice_folder_deleted: Ordner gelöscht + error_folder_title_is_already_used: Titel wird schon benutzt. Denken Sie sich etwas Neues aus. + notice_folder_details_were_saved: Ordnerdetails wurden gespeichert + error_folder_is_locked: Ordner ist gesperrt + error_file_is_locked: Datei ist gesperrt + notice_file_deleted: Datei gelöscht + error_at_least_one_revision_must_be_present: Es muss mindestens eine Version existieren + notice_revision_deleted: Version gelöscht + notice_revision_obsoleted: Revision veraltet + warning_one_of_files_locked: Eine der Dateien ist gesperrt + notice_file_revision_created: Dateiversion erstellt + notice_your_preferences_were_saved: Ihre Einstellungen wurden gespeichert + notice_your_preferences_were_not_saved: Ihre Einstellungen wurden nicht gespeichert + warning_folder_notifications_already_activated: Ordnerbenachrichtigungen sind schon aktiviert + + notice_folder_notifications_activated: Ordnerbenachrichtigungen aktiviert + warning_folder_notifications_already_deactivated: Ordnerbenachrichtigungen sind schon deaktiviert + + notice_folder_notifications_deactivated: Ordnerbenachrichtigungen deaktiviert + warning_file_notifications_already_activated: Dateibenachrichtigungen sind schon aktiviert + + notice_file_notifications_activated: Dateibenachrichtigungen aktiviert + warning_file_notifications_already_deactivated: Dateibenachrichtigungen sind schon deaktiviert + + notice_file_notifications_deactivated: Dateibenachrichtigungen deaktiviert + link_details: "%{title} Details" + link_edit: "Bearbeite %{title}" + link_create_folder: Ordner erstellen + link_title: Titel + link_size: Größe + link_modified: Geändert + link_ver: Version + link_author: Autor + title_check_for_zip_download_or_email: Wähle für ZIP-Download bzw. E-Mail + title_check_for_restore_or_delete: Wähle für Rückstellen oder Löschen + + title_notifications_active_deactivate: "Benachrichtigungen sind aktiv: Ausschalten" + title_notifications_not_active_activate: "Benachrichtigungen sind nicht aktiv: Einschalten" + title_title_version_version_download: "%{title} Version %{version} Download" + title_locked_by_user: "Gesperrt von %{user}" + title_waiting_for_approval: Warte auf Zustimmung + title_approved: Zugestimmt + title_unlock_file: Hebe Sperre auf, um Änderungen anderer Nutzer zu ermöglichen + title_lock_file: Sperre, um Änderungen anderer Nutzer zu verhindern + title_download_checked: Download der ausgewählten Dateien in einem ZIP-Archiv + title_send_checked_by_email: Sende gewählte Dateien per E-Mail + link_user_preferences: DMS Einstellungen + heading_send_documents_by_email: Sende Dateien per E-Mail + label_email_from: Von + label_email_to: An + label_email_cc: CC + label_email_subject: Betreff + label_email_documents: Dateien + label_email_body: Text + label_email_send: Senden + title_notifications_active: Benachrichtigungen sind aktiv + label_upload: Upload + heading_new_folder: Neuer Ordner + label_title: Titel + label_description: Beschreibung + submit_save: Speichern + info_file_locked: Datei ist gesperrt! + label_notifications: Benachrichtigungen + select_option_default: Voreinstellung + select_option_deactivated: Aus + select_option_activated: Ein + label_title_format: Titelformat + text_title_format: "Format für den Titel eines Dokuments beim erstmaligen Speichern (%t - Titel, %f - Datei, %d - Datum, %v - Version, %i - + ID, %r - Revision). z.B.: %t_%v" + title_save_preferences: Einstellungen speichern + heading_revisions: Versionen + title_download: Herunterladen + title_delete_revision: Version löschen + title_obsolete_revision: Veraltete Revision + label_created: Erstellt + label_changed: Geändert + info_changed_by_user: "%{changed} von" + label_filename: Dateiname + label_mime: Mime + label_size: Größe + heading_new_revision: Neue Version + option_version_same: Gleiche Version + option_version_patch: Patchversion + option_version_minor: Unterversion + option_version_major: Hauptversion + label_new_content: Neuer Inhalt + note_maximum_number_of_files_uploaded: Beschränkt die maximale Anzahl der Dateien, die auf einmal hochgeladen werden + können. 0 bedeutet unbeschränkt. + label_maximum_files_download: Maximal zulässige Anzahl der Dateien, die heruntergeladen werden können. + note_maximum_number_of_files_downloaded: Beschränkt die maximale Anzahl, der Dateien, die auf einmal heruntergeladen + werden können. (per ZIP oder E-Mail). 0 bedeutet unbeschränkt. + label_file_storage_directory: Verzeichnis für die Dateiablage + label_index_database: Index Datenbank + label_stemming_language: Stemming-Sprache + note_possible_values: Mögliche Werte + note_pass_none_to_disable_stemming: "schreibe 'none', um die Normalformreduktion zu unterdrücken" + label_stem_strategy: Normalformreduktionsform + option_stem_none: NICHTS (Voreinstellung) + option_stem_some: MANCHES + option_stem_all: ALLES + text_stemming_info: "Dies kontrolliert wie der Parser den Stemming-Algorithmus anwendet. Der Standardwert ist 'NICHTS'. + Die möglichen Werte sind: 'NICHTS' - Kein Stemming anwenden, 'MANCHES' - Suche nach Stammformreduktionen von Begriffen + mit Ausnahme von Begriffen die mit Großbuchstaben starten oder die bestimmte Zeichen enthalten ('/@<>=*[{\"'), + oder bei welchen mathematische Operatoren vorkommen. Begriffe in der Stammformreduktion weisen ein führendes 'Z' auf, + 'ALLES' - Suche nach allen Stammformreduktionen von sämtlichen Begriffen (Hinweis: es ist kein führendes 'Z' vorhanden)." + label_default_notifications: Standardmäßige Dateibenachrichtigungen + heading_uploaded_files: Hochgeladene Dateien + link_documents: Dateien + permission_view_dmsf_file_revision_accesses: Dokumentzugriffe in Aktivitäten anzeigen + permission_view_dmsf_file_revisions: Dokumentversion in Aktivitäten anzeigen + permission_view_dmsf_folders: Dateien und Ordner durchsuchen + permission_user_preferences: Benutzereinstellungen + permission_view_dmsf_files: Dateien anzeigen + permission_folder_manipulation: Ordner bearbeiten + permission_file_manipulation: Dateien bearbeiten + permission_force_file_unlock: Erzwinge Aufhebung der Dateisperre + permission_manage_workflows: Workflows verwalten + permission_file_delete: Datei löschen + permission_display_system_folders: Systemordner anzeigen + permission_file_approval: Dateien freigeben + permission_email_documents: E-Mailversand von Dokumenten + label_file: Datei + field_folder: Ordner + error_file_commit_require_uploaded_file: Das Hinzufügen erfordet eine hochgeladene Datei + warning_some_files_were_not_committed: "Einige Dateien wurden wegen Validierungsfehlern nicht hinzugefügt: %{files}" + error_user_has_not_right_delete_folder: Der Nutzer hat kein Recht den Ordner zu löschen. + error_user_has_not_right_delete_file: Der Nutzer hat kein Recht die Datei zu löschen. + notice_entries_deleted: Einträge löschen + warning_some_entries_were_not_deleted: "Einige Einträge wurden nicht gelöscht: %{entries}" + title_delete_checked: Löschen ausgewählt + title_items: Objekte + title_filename_for_download: Dateiname beim Herunterladen oder in ZIP-Archiv verwenden + label_number_of_folders: Ordner + label_number_of_documents: Dokumente + error_file_storage_directory_does_not_exist: Der Dateiablageordner existiert nicht auf dem Server und kann nicht + erstellt werden. + error_file_can_not_be_created: Datei kann nicht in dem gewählten Ordner erstellt werden. + error_wrong_zip_encoding: Falsche ZIP-Kodierung + warning_xapian_not_available: Xapian steht nicht zur Verfügung + menu_dmsf: DMS # Project tab title + label_physical_file_delete: Datei physisch löschen + user_is_not_project_member: Sie sind kein Projektmitglied + heading_access_downloads_emails: Downloads oder E-Mailversand + heading_access_first: Erste + heading_access_last: Letzte + label_dmsf_updated: aktualisiert + label_dmsf_downloaded: gespeichert + title_total_size_of_all_files: Gesamtgröße aller Dateien in diesem Ordner + project_module_dmsf: DMS # Project module name + warning_no_project_to_copy_file_to: Kein Projekt, in das die Datei kopiert werden kann. + comment_copied_from: "Kopiert aus %{source}" + field_target_project: Zielprojekt + field_target_folder: Zielordner + title_copy_or_move: Kopieren/Verschieben + label_dmsf_folder_plural: DMS Ordner # Search options + comment_moved_from: "Verschoben aus %{source}" + error_target_folder_same: Zielordner und Projekt sind unverändert. + title_copy: Kopieren + + error_max_email_filesize_exceeded: "Maximale Dateigröße der Anlage wurde überschritten. (%{number} MB)" + + note_maximum_email_filesize: Maximale Dateigröße der Anhänge, die per E-Mail verschickt werden können. 0 bedeutet + keinen Limit. Angabe in MB. + label_maximum_email_filesize: Maximale Dateigröße der Anhänge + header_minimum_filesize: Dateifehler wegen minimaler Dateigröße. + error_minimum_filesize: "Die Datei %{file} ist 0 Bytes groß und wird deswegen nicht angehängt." + parent_directory: übergeordnetes Verzeichnis + note_webdav: "Nach der Aktivierung von WebDav kann der Dienst über die URL + %{protocol}://%{domain}/dmsf/webdav/[project identifier] erreicht werden." + label_copy_dmsf: "Kopieren von Dateien und Ordnern (%{files} Dateien in %{folders} Ordnern)" + label_copy_only_dmsf_folders: "Kopieren nur von Ordnern (%{folders})" + + warning_folder_already_locked: Dieser Ordner ist bereits gesperrt + notice_folder_locked: Der Ordner wurde erfolgreich gesperrt + warning_folder_not_locked: Der Ordner konnte nicht gesperrt werden + notice_folder_unlocked: Der Ordner wurde erfolgreich entsperrt + error_only_user_that_locked_folder_can_unlock_it: Sie haben keine Berechtigung zur Entsperrung des Ordners + + title_unlock_folder: Ordner zur Bearbeitung durch andere Benutzer entsperren + title_lock_folder: Ordner zum Schutz vor Bearbeitung durch andere Benutzer sperren + + select_option_webdav_readonly: nur Lesen + select_option_webdav_readwrite: Lesen/Schreiben + label_webdav_strategy: Webdav Strategie + + note_webdav_strategy: Erlaubt dem Administrator den Wechsel der WebDav Rolle für die Endbenutzer zwischen Nur-Lesen + oder Lesen-und-Schreiben. + + error_unable_delete_dmsf_workflow: Konnte den Workflow nicht löschen + error_empty_note: Die Notiz darf nicht leer sein + error_workflow_assign: Es trat ein Fehler beim Zuweisen des Workflows auf + error_cannot_start_workflow: Workflow kann nicht gestartet werden + error_cannot_renumber_steps: Schritte können nicht umsortiert werden + label_dmsf_workflow_new: Neuer Genehmigungs-Workflow + field_label_dmsf_workflow: Genehmigungs-Workflow + field_label_dmsf_workflow_name: Genehmigungs-Workflow Name + label_dmsf_workflow_plural: Genehmigungs-Workflows + label_dmsf_workflow_plural_num: Kopieren Genehmigungs-Workflows (%{count}) + label_dmsf_workflow_step: Schritt + label_dmsf_workflow_step_plural: Schritte + label_dmsf_workflow_approval_plural: Genehmigungen + label_dmsf_wokflow_action_approve: Genehmigen + label_dmsf_wokflow_action_reject: Ablehnen + label_dmsf_wokflow_action_delegate: Delegieren an + label_dmsf_wokflow_action_assign: Weisen Sie einen Genehmigungs-Workflow zu + label_dmsf_wokflow_action_start: Starte Genehmigungs-Workflow + label_dmsf_workflow_add_approver: "Fügen Sie einen neuen Genehmiger mit einer logischen Funktion hinzu:" + label_or: oder + label_action: Aktion + label_note: Notiz + title_none: Kein Workflow + title_rejection: Ablehnung + title_delegation: Delegierung + title_assignment: Workflow zuordnen + title_start: Workflow starten + title_dmsf_workflow_log: Genehmigungs-Workflow Verlauf + title_assigned: Zugewiesen + title_approval: Dokument genehmigen + title_rejected: Abgelehnt + title_obsolete: Veraltet + dmsf_and: UND + dmsf_or: ODER + dmsf_new_step: Neuer Schritt + dmsf_new_step_or_approver: Neuer Schritt oder Neuer Genehmiger + message_dmsf_wokflow_note: Ihre Notiz... + info_revision: "r %{rev}" + link_workflow: Workflow + notice_workflow_started: Genehmigungs-Workflow gestartet + text_email_subject_approved: genehmigt + text_email_subject_rejected: abgelehnt + text_email_subject_delegated: delegiert + text_email_subject_requires_approval: benötigt Ihre Genehmigung + text_email_subject_updated: bearbeitet + text_email_subject_started: gestartet + text_email_finished_approved: "Der Genehmigungs-Workflow '%{name}' zugewiesen an die Datei '%{filename}' ist + abgeschlossen und die Datei wurde genehmigt." + text_email_finished_rejected: "Der Genehmigungs-Workflow '%{name}' zugewiesen an die Datei '%{filename}' ist + abgeschlossen, aber die Datei wurde abgelehnt, weil: '%{notice}'." + text_email_finished_delegated: "Der Genehmigungs-Workflow '%{name}' zugewiesen an die Datei '%{filename}' wurde an + Sie delegiert, weil: '%{notice}' und weil Ihre Zustimmung im aktuellen Genehmigungsschritt benötigt wird." + text_email_finished_step: "Der Genehmigungs-Workflow '%{name}' zugewiesen an die Datei '%{filename}' hat gerade einen + Zustimmungsschritt abgeschlossen und im nächsten Genehmigungsschritt wird Ihre Zustimmung benötigt." + text_email_finished_step_short: "Der Genehmigungs-Workflow '%{name}' zugewiesen an die Datei '%{filename}' hat gerade + einen Genehmigungsschritt abgeschlossen." + text_email_started: "Der Genehmigungs-Workflow '%{name}' zugewiesen an '%{filename}' wurde gestartet und im aktuellen + Genehmigungsschritt wird Ihre Zustimmung benötigt." + text_email_to_proceed: Um fortzufahren klicken Sie auf das Häckchen neben der Datei in + text_email_to_see_history: Um den Verlauf des Genehmigungs-Workflows zu sehen klicken Sie auf den Workflowstatus zur Datei + in + text_email_to_see_status: Um den aktuellen Status des Genehmigungs-Workflows zu sehen klicken Sie auf den Workflowstatus + zur Datei in + + title_create_link: Verknüpfung anlegen + label_link_from: Verknüpfung anlegen + label_link_to: Verknüpfung anlegen + label_notifications_on: Benachrichtigungen ein + label_notifications_off: Benachrichtigungen aus + field_target_file: Quelldatei + title_download_entries: Downloads anzeigen + label_external: Extern + label_internal: Internal + + label_link_name: Name der Verknüpfung + field_external_url: URL + label_target_folder: Zielordner + label_source_folder: Quellordner + label_target_project: Zielprojekt + label_source_project: Quellprojekt + + text_email_doc_updated_subject: Dokumente wurden aktualisiert + text_email_doc_updated: hat folgende Dokumente bearbeitet + text_email_doc_follows: wie folgt + text_email_doc_deleted_subject: Dokumente wurden gelöscht + text_email_doc_deleted: hat folgende Dokumente gelöscht + label_links_only: nur Verknüpfungen + + label_display_notified_recipients: Zeige benachrichtigte Empfänger + note_display_notified_recipients: Der Benutzer wird darüber informiert, wer die Empfänger der E-Mail-Benachrichtigungen sind. + warning_email_notifications: "E-Mailbenachrichtigung wurde gesendet an %{to}" + + link_trash_bin: Papierkorb + title_restore: Wiederherstellen + notice_dmsf_file_restored: Das Dokument konnte erfolgreich wiederhergestellt werden. + notice_dmsf_folder_restored: Der Ordner konnte erfolgreich wiederhergestellt werden. + notice_dmsf_link_restored: Der Link konnte erfolgreich wiederhergestellt werden. + title_restore_checked: Wiederherstellung geprüft + error_parent_folder: "Der übergeordnete Ordner existiert nicht" + + error_resource_or_parent_locked: Sperrung nicht möglich - die Ressource ist bereits gesperrt + error_parent_locked: Sperrung nicht möglich - die übergeordnete Ressource ist bereits gesperrt + error_resource_locked: Sperrung nicht möglich - die Ressource ist bereits gesperrt + error_lock_exclusively: Exklusive Sperrung nicht möglich - die Ressource ist bereits gesperrt + error_unlock_parent_locked: Entsperrung fehlgeschlagen - übergeordnete Ressource ist gesperrt + + label_dmsf_version: Version + + locked_documents: Gesperrte Dateien + open_approvals: Offene Genehmigungs-Workflows + watched_documents: Beobachtene Dokumente + + error_maximum_upload_filecount: "Es können nicht mehr als %{filecount} Datei(en) hochgeladen werden." + + label_public_urls: Öffentliche URLs gültig bis + + label_webdav: WebDAV + label_full_text: Volltext-Suche + link_extension: Ext + + label_webdav_ignore: Zu ignorierende Dateien + note_webdav_ignore: Regulärer Ausdruck (regular expression) mit Dateinamen, die bei PUT-Requests ignoriert werden. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: Keine Versionen erstellen bei + note_webdav_disable_versioning: Regulärer Ausdruck (regular expression) bei denen die Datei-Versionierung ausgeschaltet wird. Die Standardeinstellung + berücksichtigt temporäre Dateien von MS Office. + + label_dmsf_keep_documents_locked: Dokumente gesperrt halten + note_dmsf_keep_documents_locked: Dokumente werden nach der Genehmigung gesperrt gelassen + note_global: (global) + field_dmsf_not_inheritable: Nicht vererbbar + + label_webdav_use_project_names: Projektname für den Projektordner verwenden + note_webdav_use_project_names: Anstelle der Projektkennung wird der Projektname als Projektordner im Dateisystem verwendet. + + label_last_approver: Letzter Genehmiger + + label_act_as_attachable: DMS Anhänge + note_dmsf_act_as_attachable: Erlaubt das Hinzufügen von Dokumenten zu anderen Objekten (z.B. Tickets) + + label_user_search_add: Benutzer suchen + + label_dmsf_attachments: DMS Anhänge + label_basic_attachments: Dateianhänge + + label_email_from_override: Von + text_email_from_override: Der angemeldete Benutzer + label_email_reply_to: Antwort an + + 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" + + 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 + auf den Zielordner zu erstellen. + + label_dmsf_permissions: Zugriff ausschließlich erlaubt für + label_inherited_permissions: Zugriff vererbt für + + button_edit_content: Dokument bearbeiten + field_workflow: Workflow + field_modified: Datum + field_updated: Datum + field_count: Anzahl Downloads + field_first_at: Erste + field_last_at: Letzte + field_size: Größe + field_locked: Gesperrt + + label_add_width: Zugeben mit + + dmsf_webdav_ignore_1b_file_for_authentication: Ignoriere 1b Datei für Authorisierung + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: Der Ordner ist nicht leer. + label_scroll_down: Runterscrollen + note_webdav_disabled: WebDAV ist deaktiviert. Administrator kontaktieren. + + dmsf_copy: "Kopie (%{n})" + label_empty_trash_bin: Papierkorb leeren + label_dmsf_projects_as_subfolders: Unterprojekte als Unterordner + note_dmsf_projects_as_subfolders: Unterprojekte als Unterordner in die DMS View hinzufügen + only_approval_zero_minor_version: Genehmigungen nur für Unterversion Null + title_assignment_minor: Zuordnung nicht gestattet, Unterversion muss Null lauten + title_start_minor: Start nicht erlaubt, Unterversion muss Null lauten + title_approval_minor: Genehmigung nicht erlaubt, Unterversion muss Null lauten + + label_project_watchers: Beobachter + label_dmsf_folder_watchers: Beobachter + label_dmsf_file_watchers: Beobachter + label_dmsf_watched: Beobachtete Dokumente + label_dmsf_legacy_notifications: DMS Dokument oder Verzeichnis aktualisiert + permission_view_dmsf_folder_watchers: Liste der Beobachter von Verzeichnissen ansehen + permission_add_dmsf_folder_watchers: Beobachter von Verzeichnissen hinzufügen + permission_delete_dmsf_folder_watchers: Beobachter von Verzeichnissen löschen + permission_view_dmsf_file_watchers: Liste der Beobachter eines Dokuments ansehen + permission_add_dmsf_file_watchers: Beobachter von Dokumenten hinzufügen + permission_delete_dmsf_file_watchers: Beobachter von Dokumenten löschen + permission_view_project_watchers: Liste der Beobachter vom DMS ansehen + permission_add_project_watchers: Beobachter vom DMS hinzufügen + permission_delete_project_watchers: Beobachter vom DMS löschen + label_dmsf_new_top_level_document: Neues DMS-Dokument auf oberster Ebene hinzufügen + label_dmsf_new_top_level_folder: Neues DMS-Verzeichnis auf oberster Ebene hinzufügen + + label_dmsf_max_notification_receivers_info: Maximale Anzahl von E-Mail-Empfängern + note_dmsf_max_notification_receivers_info: Begrenzt die Anzahl der angezeigten Empfänger von E-Mail-Benachrichtigungen. + label_dmsf_office_bin: LibreOffice Binärdatei + note_dmsf_office_bin: "Eine Binärdatei zum Umwandeln von Office-Dokumenten in ein PDF, um so eine Vorschau anzeigen zu können. + Wenn Sie keine Vorschau anzeigen lassen möchten, geben Sie einen leeren String ein: ''. Der Server muss neu + gestartet werden, damit die Änderung wirksam wird." + note_dmsf_office_bin_not_available: "LibreOffice's Binärdatei für die Kommandozeile '%{value}' ist nicht verfügbar." + + label_dmsf_columns: DMS Spalten + label_column_id: ID + label_column_title: Titel + label_column_size: Größe + label_column_modified: Geändert + label_column_version: Version + label_column_workflow: Workflow + label_column_author: Autor + label_column_description: Beschreibung + label_column_comment: Kommentar + + label_dmsf_global_menu_disabled: Globales DMS Menü deaktiviert + note_dmsf_global_menu_disabled: Falls ja, wird das DMS Menüelement im Top Menü nicht erscheinen. + error_dmsf_workflow_assigned: Der verwendete Genehmigungs-Workflow kann weder bearbeitet noch gelöscht werden. + + label_empty_minor_version_by_default: Leere Unterversion als Standard + text_email_doc_downloaded_subject: Dokumenten wurden herunterladen + text_email_doc_downloaded: hat folgende Dokumente herunterladen + field_default_dmsf_query: Standard-DMS-Abfrage + field_receive_download_notification: Benachrichtigung über heruntergeladene Dokumente + + label_remove_original_documents_module: Original Projektmodule Dokumente entfernen + + notice_entries_copied: Kopieren ist gelungen + notice_entries_moved: Verschieben ist gelungen + label_dmsf_file_revision: DMS Dokument Rev. + error_not_supported_image_format: Nicht unterstütztes Bildformat + error_not_supported_video_format: Nicht unterstütztes Videoformat + + label_webdav_authentication: WebDAV Authentifizierung + note_webdav_authentication: Die Basis-Authentifizierungsmethode gilt als unsicher und wird daher von einigen Clients blockiert + Die Digest-Authentifizierung basiert auf einem automatisch generierten Digest. Die Benutzer verwenden ihre Anmeldung und ihr Kennwort zur + Authentifizierung auch in ihren WebDAV-Clients. + label_dmsf_webdav_digest_created_on: "DMS WebDAV Digest vor %{value} erstellt" + label_missing_dmsf_webdav_digest: DMS WebDAV Digest fehlt + label_dmsf_webdav_digest: DMS WebDAV Digest + text_dmsf_webdav_digest_reset: Bitte geben Sie Ihr Passwort ein, um einen neuen DMS WebDAV Digest zu generieren. + notice_webdav_digest_reset: Ihr DMS WebDAV Digest wurde zurückgesetzt. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Hochladen und Commit + + notice_search_in_subfolders: Die Suche in Unterordnern ist nicht rekursiv. Für eine rekursive Suche gehen Sie auf die + oberste Ebene. + warning_folder_unlockable: Der Ordner kann nicht entsperrt werden + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: enthält ungültige Zeichen diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 00000000..b8032cab --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +en: + dmsf: DMS # Custom fields tab title + label_dmsf_file: DMS Document + label_dmsf_file_plural: DMS Documents # Email subject & Search options + label_dmsf_file_revision_plural: Document revisions + label_dmsf_file_revision_access_plural: Document accesses + warning_no_entries_selected: No entries selected + error_email_to_must_be_entered: Email To must be entered + warning_file_already_locked: File already locked + notice_file_locked: File locked + warning_file_not_locked: File not locked + notice_file_unlocked: File unlocked + error_only_user_that_locked_file_can_unlock_it: Only user that locked the file can unlock it + + error_max_files_exceeded: "Limit for %{number} simultaneously downloaded files exceeded" + error_entry_project_does_not_match_current_project: "Entry project doesn't match current project" + + notice_folder_created: Folder created + error_folder_creation_failed: Folder creation failed + error_folder_title_must_be_entered: Title must be entered + notice_folder_deleted: Folder deleted + error_folder_title_is_already_used: Title is already used + notice_folder_details_were_saved: Folder details were saved + error_folder_is_locked: Folder is locked + error_file_is_locked: File is locked + notice_file_deleted: File deleted + error_at_least_one_revision_must_be_present: At least one revision must be present + notice_revision_deleted: Revision deleted + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: One of files locked + notice_file_revision_created: File revision created + notice_your_preferences_were_saved: Your preferences were saved + notice_your_preferences_were_not_saved: Your preferences were not saved + warning_folder_notifications_already_activated: Folder notifications already activated + + notice_folder_notifications_activated: Folder notifications activated + warning_folder_notifications_already_deactivated: Folder notifications already deactivated + + notice_folder_notifications_deactivated: Folder notifications deactivated + warning_file_notifications_already_activated: File notifications already activated + + notice_file_notifications_activated: File notifications activated + warning_file_notifications_already_deactivated: File notifications already deactivated + + notice_file_notifications_deactivated: File notifications deactivated + link_details: "%{title} details" + link_edit: "Edit %{title}" + link_create_folder: Create folder + link_title: Title + link_size: Size + link_modified: Modified + link_ver: Ver. + link_author: Author + title_check_for_zip_download_or_email: Check for zip, download or email + title_check_for_restore_or_delete: Check for restore or delete + + title_notifications_active_deactivate: "Notifications active: Deactivate" + title_notifications_not_active_activate: "Notifications not active: Activate" + title_title_version_version_download: "%{title} version %{version} download" + title_locked_by_user: "Locked by %{user}" + title_waiting_for_approval: Waiting for Approval + title_approved: Approved + title_unlock_file: Unlock to allow changes for other members + title_lock_file: Lock to prevent changes for other members + title_download_checked: Download checked in Zip archive + title_send_checked_by_email: Send checked by email + link_user_preferences: Your DMS project preferences + heading_send_documents_by_email: Send documents by email + label_email_from: From + label_email_to: To + label_email_cc: CC + label_email_subject: Subject + label_email_documents: Documents + label_email_body: Body + label_email_send: Send + title_notifications_active: Notifications active + label_upload: Upload + heading_new_folder: New Folder + label_title: Title + label_description: Description + submit_save: Save + info_file_locked: File locked! + label_notifications: Notifications + select_option_default: Default + select_option_deactivated: Deactivated + select_option_activated: Activated + label_title_format: Title format + text_title_format: "Document title format for download (%t - title, %f - file, %d - date, %v - version, %i - ID, %r - + revision). Example: %t_%v" + title_save_preferences: Save preferences + heading_revisions: Revisions + title_download: Download + title_delete_revision: Delete revision + title_obsolete_revision: Obsolete revision + label_created: Created + label_changed: Changed + info_changed_by_user: "%{changed} by" + label_filename: Filename + label_mime: Mime + label_size: Size + heading_new_revision: New Revision + option_version_same: Same + option_version_patch: Patch + option_version_minor: Minor + option_version_major: Major + option_version_custom: Custom + label_new_content: New content + label_maximum_files_download: Maximum files download + note_maximum_number_of_files_downloaded: Limits maximum number of files downloaded in zip or sent via email. 0 means + unlimited. + label_file_storage_directory: File storage directory + label_index_database: Index database + label_stemming_language: Stemming language + note_possible_values: Possible values + note_pass_none_to_disable_stemming: "pass 'none' to disable stemming" + label_stem_strategy: Stem strategy + option_stem_none: Stem none (default) + option_stem_some: Stem some + option_stem_all: Stem all + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: File default notifications + heading_uploaded_files: Uploaded Files + link_documents: Documents + permission_view_dmsf_file_revision_accesses: View downloads in Activity stream + permission_view_dmsf_file_revisions: View revisions in Activity stream + permission_view_dmsf_folders: Browse documents + permission_user_preferences: User preferences + permission_view_dmsf_files: View documents + permission_folder_manipulation: Folder manipulation + permission_file_manipulation: File manipulation + permission_force_file_unlock: Force file unlock + permission_manage_workflows: Manage workflows + permission_file_delete: Delete documents + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: File + field_folder: Folder + error_file_commit_require_uploaded_file: File commit require uploaded file + + warning_some_files_were_not_committed: "Some files were not committed due to validation errors: %{files}" + + error_user_has_not_right_delete_folder: "User hasn't right to delete folders" + + error_user_has_not_right_delete_file: "User hasn't right to delete file" + + notice_entries_deleted: Entries deleted + warning_some_entries_were_not_deleted: "Some entries weren't deleted: %{entries}" + title_delete_checked: Delete checked + title_items: items + title_filename_for_download: Filename used for download or in Zip archive + label_number_of_folders: Folders + label_number_of_documents: Documents + error_file_storage_directory_does_not_exist: "File storage directory doesn't exist and can't be created" + + error_file_can_not_be_created: "Files can't be created in storage directory" + error_wrong_zip_encoding: Wrong Zip encoding + warning_xapian_not_available: Xapian not available + menu_dmsf: DMS # Project tab title + label_physical_file_delete: Physical file delete + user_is_not_project_member: You are not a member of the project + heading_access_downloads_emails: Downloads/Emails + heading_access_first: First + heading_access_last: Last + label_dmsf_updated: Updated + label_dmsf_downloaded: Downloaded + title_total_size_of_all_files: Total size of all files under this folder + project_module_dmsf: DMS # Project module name + warning_no_project_to_copy_file_to: No project to copy file to + comment_copied_from: "Copied from %{source}" + field_target_project: Target project + field_target_folder: Target folder + title_copy_or_move: Copy/Move + label_dmsf_folder_plural: DMS Folders # Search options + comment_moved_from: "Moved from %{source}" + error_target_folder_same: Target folder and project are the same as the current one. + title_copy: Copy + + error_max_email_filesize_exceeded: "You've exceeded the maximum filesize for sending via email. (%{number} MB)" + + note_maximum_email_filesize: Limits maximum filesize that can be sent via email. 0 means unlimited. Number is in MB. + + label_maximum_email_filesize: Maximum email attachment size + header_minimum_filesize: File Error. + error_minimum_filesize: "The file %{file} is 0 bytes and will not be attached." + parent_directory: Parent Directory + note_webdav: "Webdav once enabled can be found at %{protocol}://%{domain}/dmsf/webdav/[project identifier]" + + label_copy_dmsf: "Copy documents and folders (%{files} files in %{folders} folders)" + label_copy_only_dmsf_folders: "Copy folders only (%{folders})" + + warning_folder_already_locked: This folder is already locked + notice_folder_locked: The folder was successfully locked + warning_folder_not_locked: Unfortunately, the folder could not be locked + notice_folder_unlocked: The folder was successfully unlocked + error_only_user_that_locked_folder_can_unlock_it: You are not authorised to unlock this folder + + title_unlock_folder: Unlock to allow changes for other members + title_lock_folder: Lock to prevent changes for other members + + select_option_webdav_readonly: Read-only + select_option_webdav_readwrite: Read/Write + label_webdav_strategy: Webdav strategy + + note_webdav_strategy: Enables the administrator to decide if webdav is a read-only or read-write platform for end + users. + + error_unable_delete_dmsf_workflow: Unable to delete the workflow + error_empty_note: "The note can't be empty" + error_workflow_assign: An error occurred while assigning + error_cannot_start_workflow: "Workflow can't be started" + error_cannot_renumber_steps: "Steps can't be renumbered" + label_dmsf_workflow_new: New approval workflow + field_label_dmsf_workflow: Approval Workflow + field_label_dmsf_workflow_name: Approval workflow name + label_dmsf_workflow_plural: Approval workflows + label_dmsf_workflow_plural_num: Copy approval workflows (%{count}) + label_dmsf_workflow_step: Step + label_dmsf_workflow_step_plural: Steps + label_dmsf_workflow_approval_plural: Approvals + label_dmsf_wokflow_action_approve: Approve + label_dmsf_wokflow_action_reject: Reject + label_dmsf_wokflow_action_delegate: Delegate to + label_dmsf_wokflow_action_assign: Assign an approval workflow + label_dmsf_wokflow_action_start: Start workflow + label_dmsf_workflow_add_approver: "Add a new approver with a logical function:" + label_or: or + label_action: Action + label_note: Note + title_none: None + title_rejection: Rejection + title_delegation: Delegation + title_assignment: Assign workflow + title_start: Start workflow + title_dmsf_workflow_log: Approval Workflow Log + title_assigned: Assigned + title_approval: Approve document + title_rejected: Rejected + title_obsolete: Obsolete + dmsf_and: AND + dmsf_or: OR + dmsf_new_step: New step + dmsf_new_step_or_approver: New step or New Approver + message_dmsf_wokflow_note: Your note... + info_revision: "r %{rev}" + link_workflow: Workflow + notice_workflow_started: Approval workflow successfully started + text_email_subject_approved: approved + text_email_subject_rejected: rejected + text_email_subject_delegated: delegated + text_email_subject_requires_approval: requires your approval + text_email_subject_updated: updated + text_email_subject_started: started + text_email_finished_approved: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + finished and the document has been approved." + text_email_finished_rejected: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + finished and the document has been rejected because of '%{notice}'." + text_email_finished_delegated: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + delegated because of '%{notice}' and you are expected to do an approval in the current approval step '%{stepname}'." + text_email_finished_step: "The approval workflow '%{name}' assigned to '%{filename}' document has just finished one + of the approval steps and you are expected to do an approval in the next approval step." + text_email_finished_step_short: "The approval workflow '%{name}' assigned to '%{filename}' document has just finished + one of the approval steps." + text_email_started: "The approval workflow '%{name}' assigned to '%{filename}' document has just been started and you + are expected to do an approval in the current approval step '%{stepname}'." + text_email_to_proceed: To proceed click on the check box icon next to the document in + text_email_to_see_history: To see the approval history click on the workflow status of the document in + + text_email_to_see_status: To see the current status of the approval workflow click on the workflow status the document + in + + title_create_link: Create a symbolic link + label_link_from: Link from + label_link_to: Link to + label_notifications_on: Turn on notifications + label_notifications_off: Turn off notifications + field_target_file: Source file + title_download_entries: Download entries + label_external: External + label_internal: Internal + + label_link_name: Link name + field_external_url: URL + label_target_folder: Target folder + label_source_folder: Source folder + label_target_project: Target project + label_source_project: Source project + + text_email_doc_updated_subject: Documents updated + text_email_doc_updated: has just actualized documents of + text_email_doc_follows: as follows + text_email_doc_deleted_subject: Documents deleted + text_email_doc_deleted: has just deleted documents of + label_links_only: links only + + label_display_notified_recipients: Display notified recipients + note_display_notified_recipients: The user will be informed about all recipients of just sent the email notification. + + warning_email_notifications: "Email notifications sent to %{to}" + + link_trash_bin: Trash bin + title_restore: Restore + notice_dmsf_file_restored: The document has been successfully restored + notice_dmsf_folder_restored: The folder has been successfully restored + notice_dmsf_link_restored: The link has been successfully restored + title_restore_checked: Restore checked + error_parent_folder: "The parent folder doesn't exist" + + error_resource_or_parent_locked: Unable to complete lock - resource (or parent) is locked + error_parent_locked: Unable to complete lock - resource parent is locked + error_resource_locked: Unable to complete lock - resource is locked + error_lock_exclusively: Unable to lock exclusively an already-locked resource + error_unlock_parent_locked: Unlock failed - resource parent is locked + + label_dmsf_version: Version + + locked_documents: Locked documents + open_approvals: Open approvals + watched_documents: Watched documents + + error_maximum_upload_filecount: "No more than %{filecount} file(s) can be uploaded." + + label_public_urls: Public URLs valid to + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Ignored files patterns + note_webdav_ignore: A regular expression with filenames to ignore by PUT requests. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. The default pattern + matches temporary files created by MsOffice. + + label_dmsf_keep_documents_locked: Keep documents locked + note_dmsf_keep_documents_locked: Documents will be kept locked when approved + note_global: (global) + field_dmsf_not_inheritable: Not inheritable + + label_webdav_use_project_names: Use project names for project folders + note_webdav_use_project_names: Use project names instead of project identifiers for project folders. + + label_last_approver: Last approver + + label_act_as_attachable: Act as attachable + note_dmsf_act_as_attachable: Allows to attach documents to objects e.g. issues. + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: Workflow + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: Size + field_locked: Locked + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: Title + label_column_size: Size + label_column_modified: Modified + label_column_version: Version + label_column_workflow: Workflow + label_column_author: Author + label_column_description: Description + label_column_comment: Comment + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: contains invalid character(s) diff --git a/config/locales/es.yml b/config/locales/es.yml new file mode 100644 index 00000000..850d6109 --- /dev/null +++ b/config/locales/es.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Agustin Ivorra , Karel Pičman +# +# 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 +# . + +es: + dmsf: DMS # Custom fields tab title + label_dmsf_file: DMS Archivo + label_dmsf_file_plural: DMS Archivos # Email subject & Search options + label_dmsf_file_revision_plural: Document revisions + label_dmsf_file_revision_access_plural: Document accesses + warning_no_entries_selected: No ha seleccionado ningún ítem + error_email_to_must_be_entered: Ingrese un email + warning_file_already_locked: El archivo ya está bloqueado + notice_file_locked: Archivo bloqueado + warning_file_not_locked: Archivo no bloqueado + notice_file_unlocked: Archivo desbloqueado + error_only_user_that_locked_file_can_unlock_it: Solo los usuarios que bloquearon previamente al archivo lo pueden + desbloquear + error_max_files_exceeded: "Se excedio el numero permitido de archivos bajados de manera simultánea:" + error_entry_project_does_not_match_current_project: Las entradas del proyecto no concuerdan con el proyecto + seleccionado + notice_folder_created: Carpeta creada satisfactoriamente + error_folder_creation_failed: La creacion de la carpeta ha fallado + error_folder_title_must_be_entered: Debe ingresar un título + notice_folder_deleted: Carpeta borrada + error_folder_title_is_already_used: El título ingresado ya está siendo usado por otro documento + notice_folder_details_were_saved: Los detalles de la carpeta fueron grabados correctamente + error_folder_is_locked: Carpeta bloqueado + error_file_is_locked: Archivo bloqueado + notice_file_deleted: Archivo borrado + error_at_least_one_revision_must_be_present: al menos una revisión debe estar presente + notice_revision_deleted: Revision eliminada correctamente + notice_revision_obsoleted: Revision obsoletada correctamente + warning_one_of_files_locked: Uno de los archivos está bloqueado + notice_file_revision_created: Revision de archivos creada correctamente + notice_your_preferences_were_saved: Sus preferencias han sido guardadas correctamente + notice_your_preferences_were_not_saved: Your preferences were not saved + warning_folder_notifications_already_activated: Las notificaciones de la carpeta seleccionada ya están activadas + previamente + notice_folder_notifications_activated: Notificaciones de carpeta activadas + warning_folder_notifications_already_deactivated: Las notificaciones de la carpeta seleccionada ya están desactivadas + previamente + notice_folder_notifications_deactivated: Notificaciones de carpeta desactivadas + warning_file_notifications_already_activated: Las notificaciones del archivo seleccionado ya estaban activadas + previamente + notice_file_notifications_activated: Notificación de archivo activado + warning_file_notifications_already_deactivated: Las notificaciones del archivo seleccionado ya estaban desactivadas + previamente + notice_file_notifications_deactivated: Notificación de archivo desactivada + link_details: "%{title} detalles" + link_edit: "Editar %{title}" + link_create_folder: Crear Directorio + link_title: Titulo + link_size: Tamaño + link_modified: Modificado + link_ver: Ver. + link_author: Autor + title_check_for_zip_download_or_email: Tildar para crear Zip o enviarlo por email + title_check_for_restore_or_delete: Check for restore or delete + + title_notifications_active_deactivate: "Notificaciones Activas: Desactivadas" + title_notifications_not_active_activate: "Notificacciones No Activas: Activadas" + title_title_version_version_download: "%{title} version %{version} descargar" + title_locked_by_user: "bloqueado por %{user}" + title_waiting_for_approval: Esperando Aprobación + title_approved: Aprobado + title_unlock_file: Desbloquear para que otros miembros puedan editarlo + title_lock_file: Bloquear para que otros miembros no puedan editarlo + title_download_checked: Descargar archivos seleccionados en Zip + title_send_checked_by_email: Enviar los seleccionados por email + link_user_preferences: Preferencias de su proyecto DMS + heading_send_documents_by_email: Enviar documentos por email + label_email_from: De + label_email_to: A + label_email_cc: CC + label_email_subject: Asunto + label_email_documents: Documentos + label_email_body: Cuerpo de Mensaje + label_email_send: Enviar + title_notifications_active: Activar Notificaciones + label_upload: Subir + heading_new_folder: Nuevo directorio + label_title: Título + label_description: Descripción + submit_save: Guardar + info_file_locked: Archivo Bloqueado! + label_notifications: Notificaciones + select_option_default: Default + select_option_deactivated: Desactivado + select_option_activated: Activado + label_title_format: Title format + text_title_format: "Document title format for download (%t - title, %f - file, %d - date, %v - version, %i - ID, %r - + revision). Example: %t_%v" + title_save_preferences: Guardar Preferencias + heading_revisions: Revisiones + title_download: Descargar + title_delete_revision: Eliminar revisión + title_obsolete_revision: Obsoletar revisión + label_created: Creado + label_changed: Modificado + info_changed_by_user: "%{changed} por" + label_filename: Nombre + label_mime: Mime + label_size: Tamaño + heading_new_revision: Nueva Revisión + option_version_same: Misma + option_version_patch: Patch + option_version_minor: Menor + option_version_major: Mayor + option_version_custom: Custom + label_new_content: Nuevo contenido + label_maximum_files_download: Número máximo de archivos para descargar + note_maximum_number_of_files_downloaded: Número de límite máximo de archivos para una sola descarga en Zip o enviado + por email. 0 equivale a ilimitdo. + label_file_storage_directory: Directorio de almacenamiento de archivos + label_index_database: Indice de base de datos + label_stemming_language: Stemming language + note_possible_values: Posibles Valores + note_pass_none_to_disable_stemming: "pass 'none' to disable stemming" + label_stem_strategy: Stem strategy + option_stem_none: Stem none (default) + option_stem_some: Stem some + option_stem_all: Stem all + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: Notificación por defecto de archivo + heading_uploaded_files: Archivos subidos + link_documents: Documentos + permission_view_dmsf_file_revision_accesses: View downloads in Activity stream + permission_view_dmsf_file_revisions: View revisions in Activity stream + permission_view_dmsf_folders: Examinar Documentos + permission_user_preferences: Preferencias de usuario + permission_view_dmsf_files: Ver documentos + permission_folder_manipulation: Manipulación de directorio + permission_file_manipulation: Manipulación de Archivos + permission_force_file_unlock: Forzar desbloqueo de archivo + permission_manage_workflows: Gesti{on de flujo de trabajo + permission_file_delete: Eliminar documentos + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: Archivo + field_folder: Directorio + error_file_commit_require_uploaded_file: El commit requiere archivos subidos + + warning_some_files_were_not_committed: "Algunos archivos no fueron registrados por un error de validación: %{files}" + + error_user_has_not_right_delete_folder: "No tiene permisos para eliminar el directorio" + + error_user_has_not_right_delete_file: "No tiene permisos para eliminar el archivo" + + notice_entries_deleted: Entradas eliminadas + warning_some_entries_were_not_deleted: "Algunas entradas no fueron eliminadas: %{entries}" + title_delete_checked: Eliminación chequeada + title_items: items + title_filename_for_download: Nombre de archivo utilizado para descargar o crear zip + label_number_of_folders: Directorios + label_number_of_documents: Documentos + error_file_storage_directory_does_not_exist: "El directorio destino no existe y no puede ser creado" + + error_file_can_not_be_created: "El archivo no puede ser creado en el directorio destino" + error_wrong_zip_encoding: Codificación erronea de Zip + warning_xapian_not_available: Xapian no disponible + menu_dmsf: DMS # Project tab title + label_physical_file_delete: Eliminar archivos físicos + user_is_not_project_member: No es miemo del proyecto + heading_access_downloads_emails: Descargas/Emails + heading_access_first: Primero + heading_access_last: Último + label_dmsf_updated: Subidas + label_dmsf_downloaded: Downloaded + title_total_size_of_all_files: Tamaño total de los archivos en el directorio + project_module_dmsf: DMS # Project module name + warning_no_project_to_copy_file_to: No hay proyecto para copiar el archivo + comment_copied_from: "Copiado desde %{source}" + field_target_project: Proyecto destino + field_target_folder: Directorio destino + title_copy_or_move: Copiar/Mover + label_dmsf_folder_plural: DMS Directorio # Search options + comment_moved_from: "Mover desde %{source}" + error_target_folder_same: El directorio y proyecto destino son el actual + title_copy: Copiar + + error_max_email_filesize_exceeded: "Tamaño máximo de archivo excedido para ser enviado por email. (%{number} MB)" + + note_maximum_email_filesize: Límite máximo de archivo para ser enviado por email. 0 equivale a ilimitado. Unidad en + MB. + label_maximum_email_filesize: Tamaño máximo de adjunto en email + header_minimum_filesize: Error en archivo. + error_minimum_filesize: "El archivo %{file} tiene 0 bytes de tamaño y no será adjuntado." + parent_directory: Directorio padre + note_webdav: "Si Webdav está habilitado, se puede encontrar en %{protocol}://%{domain}/dmsf/webdav/[project + identifier]" + label_copy_dmsf: "Copiar archivos y directorios (%{files} archivos en %{folders} directorios)" + label_copy_only_dmsf_folders: "Copiar solamente directorios (%{folders})" + + warning_folder_already_locked: El directorio ya se encuentra bloqueado + notice_folder_locked: "El directorio se bloqueó exitosamente" + warning_folder_not_locked: Desafortunadamente, el directorio no se pudo bloquear + notice_folder_unlocked: El directorio se desbloqueó exitosamente + error_only_user_that_locked_folder_can_unlock_it: No está autorizado para desbloquearel directorio + + title_unlock_folder: Desbloquear para que sea editado por otros miembros + title_lock_folder: Bloquear para evitar que sea editado por otros miembros + + select_option_webdav_readonly: Solo Lectura + select_option_webdav_readwrite: Lectura/Escritura + label_webdav_strategy: Estrategia Webdav + + note_webdav_strategy: Habilitar el administrador para configurar si la plataforma webdav es solo lectura o + lectura-escritura para los usuarios finales. + + error_unable_delete_dmsf_workflow: Incapaz de eliminar el flujo de trabajo + error_empty_note: "La nota no puede estar vacía" + error_workflow_assign: "Ocurió un errpr mientras se asignaba" + error_cannot_start_workflow: "No se puede iniciar el flujo de trabajo" + error_cannot_renumber_steps: "Las etapas no pueden ser enumeradas" + label_dmsf_workflow_new: "Nuevo flujo de trabajo de aprobación" + field_label_dmsf_workflow: "Flujo de trabajo de aprobación" + field_label_dmsf_workflow_name: "Nombre de flujo de trabajo de aprobación" + label_dmsf_workflow_plural: "Flujos de trabajo de aprobación" + label_dmsf_workflow_plural_num: Copy approval workflows (%{count}) + label_dmsf_workflow_step: Paso + label_dmsf_workflow_step_plural: Pasos + label_dmsf_workflow_approval_plural: Aprobaciones + label_dmsf_wokflow_action_approve: Aprobar + label_dmsf_wokflow_action_reject: Rechazar + label_dmsf_wokflow_action_delegate: Delegar a + label_dmsf_wokflow_action_assign: "Asignar flujo de trabajo de aprobación" + label_dmsf_wokflow_action_start: Comenzar flujo de trabajo + label_dmsf_workflow_add_approver: "Añadir un nuevo aprobador con una función lógica:" + label_or: o + label_action: "Acción" + label_note: Nota + title_none: Ninguno + title_rejection: Rechazo + title_delegation: "Delegación" + title_assignment: "Asignación" + title_start: Comenzar + title_dmsf_workflow_log: "Log flujo de trabajo de aprobación" + title_assigned: Asignado + title_approval: Aprobar + title_rejected: Rechazado + title_obsolete: Obsoleto + dmsf_and: Y + dmsf_or: O + dmsf_new_step: Nuevo Paso + dmsf_new_step_or_approver: Nuevo Paso o Nuevo aprobador + message_dmsf_wokflow_note: Tu nota... + info_revision: "r %{rev}" + link_workflow: Flujo de Trabajo + notice_workflow_started: "Flujo de trabajo de aprobación iniciado satisfactoriamente" + text_email_subject_approved: aprobado + text_email_subject_rejected: rechazado + text_email_subject_delegated: delegado + text_email_subject_requires_approval: requiere su aprobación + text_email_subject_updated: actualizado + text_email_subject_started: comenzado + text_email_finished_approved: "El flujo de trabajo de aprobación '%{name}' asignado al documento '%{filename}' acaba + de ser terminado y él ha sido aprobado." + text_email_finished_rejected: "El flujo de trabajo de aprobación '%{name}' asignado al documento '%{filename}' acaba + de ser terminado y él ha sido rechazado por el siguiente motivo '%{notice}'." + text_email_finished_delegated: "El flujo de trabajo de aprobación '%{name}' asignado al documento '%{filename}' acaba + de ser delegado por '%{notice}' y se espera que haga una aprobación en la etapa de aprobación actual '%{stepname}'." + text_email_finished_step: "El flujo de trabajo de aprobación '%{name}' asignado al documento '%{filename}' acaba de + terminar uno de los pasos de aprobación y se espera que haga una aprobación en el siguiente paso." + text_email_finished_step_short: "El flujo de trabajo de aprobación '%{name}' asignado al documento '%{filename}' acaba + de finalizar uno de los pasos de aprobación." + text_email_started: "El flujo de trabajo de aprobación '%{name}' asignado al documento '%{filename}' acaba de + iniciarse y se espera que haga una aprobación en la etapa de aprobación actual '%{stepname}'." + text_email_to_proceed: "Para continuar, haga clic en el icono de la casilla de verificación al lado del documento" + text_email_to_see_history: "Para ver el historial de aprobación haga clic en el estado del flujo de trabajo del + documento" + text_email_to_see_status: "Para ver el estado actual del flujo de trabajo de aprobación, haga clic en el estado del + flujo de trabajo del documento" + + title_create_link: "Crear un enlace simbólico" + label_link_from: Enlace desde + label_link_to: Enlace hacia + label_notifications_off: Desactivar notificaciones + label_notifications_on: Activar notificaciones + field_target_file: Archivo fuente + title_download_entries: Entradas de descarga + label_external: External + label_internal: Internal + + label_link_name: Nombre de enlace + field_external_url: URL + label_target_folder: Directorio destino + label_source_folder: Directorio fuente + label_target_project: Proyecto destino + label_source_project: Proyecto fuente + + text_email_doc_updated_subject: Documentos actualizados + text_email_doc_updated: acaba de actualizar los documentos de + text_email_doc_follows: lo siguiente + text_email_doc_deleted_subject: Documentos eliminados + text_email_doc_deleted: acaba de eliminar documentos de + label_links_only: Solo enlaces + + label_display_notified_recipients: Mostrar destinatarios notificados + note_display_notified_recipients: "El usuario será informado de todos los destinatarios a los que acaba de enviar la + notificación por correo electrónico" + warning_email_notifications: "Notificaciones de email enviadas a %{to}" + + link_trash_bin: Papelera + title_restore: Recuperar + notice_dmsf_file_restored: El documento ha sido restaurado satisfactoriamente + notice_dmsf_folder_restored: El directorio ha sido restaurado satisfactoriamente + notice_dmsf_link_restored: El enlace ha sido restaurado satisfactoriamente + title_restore_checked: Restaurar Seleccionados + error_parent_folder: "El directorio padre no existe" + + error_resource_or_parent_locked: Unable to complete lock - resource (or parent) is locked + error_parent_locked: Unable to complete lock - resource parent is locked + error_resource_locked: Unable to complete lock - resource is locked + error_lock_exclusively: Unable to lock exclusively an already-locked resource + error_unlock_parent_locked: Unlock failed - resource parent is locked + + label_dmsf_version: Versión + + locked_documents: Documentos bloqueados + open_approvals: Aprobaciones abiertas + watched_documents: Watched documents + + error_maximum_upload_filecount: "No more than %{filecount} file(s) can be uploaded." + + label_public_urls: Public URLs valid to + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Ignored files patterns + note_webdav_ignore: A regular expresion with filenames to ignore by PUT requests. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. The default pattern + matches temporary files created by MsOffice. + + label_dmsf_keep_documents_locked: Keep documents locked + note_dmsf_keep_documents_locked: Documents will be kept locked when approved + note_global: (global) + field_dmsf_not_inheritable: Not inheritable + + label_webdav_use_project_names: Use project name for project folder + note_webdav_use_project_names: Use project names instead of project identifier for project folders. + + label_last_approver: Last approver + + label_act_as_attachable: Act as attachable + note_dmsf_act_as_attachable: Allows to attach documents to objects e.g. issues. + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: Flujo + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: Tamaño + field_locked: Bloqueado + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: Título + label_column_size: Tamaño + label_column_modified: Modificado + label_column_version: Versión + label_column_workflow: Flujo + label_column_author: Autor + label_column_description: Descripción + label_column_comment: Comentario + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + activerecord: + errors: + messages: + error_contains_invalid_character: contiene caracter(es) inválido(s) \ No newline at end of file diff --git a/config/locales/fa.yml b/config/locales/fa.yml new file mode 100644 index 00000000..9298d77b --- /dev/null +++ b/config/locales/fa.yml @@ -0,0 +1,477 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +fa: + dmsf: اسناد پیش‌رفته # Custom fields tab title + label_dmsf_file: DMS Document + label_dmsf_file_plural: اسناد پیش‌رفته # Email subject & Search options + label_dmsf_file_revision_plural: بازبینی‌های اسناد + label_dmsf_file_revision_access_plural: دسترسی‌های اسناد پیش‌رفته + warning_no_entries_selected: هیچ موردی انتخاب نشده است + error_email_to_must_be_entered: گیرنده رایانامه باید وارد شده باشد + warning_file_already_locked: پرونده قبلا قفل شده است + notice_file_locked: فایل قفل شده است + warning_file_not_locked: فایل قفل نیست + notice_file_unlocked: قفل پرونده باز شد + error_only_user_that_locked_file_can_unlock_it: فقط کاربری که پرونده را قفل کرده است می‌تواند آن را باز کند + + error_max_files_exceeded: "محدودیت %{number} پرونده دریافتی هم‌زمان رد شده است" + error_entry_project_does_not_match_current_project: "پروژه‌ی ورودی با پروژه‌ی جاری هم‌خوانین دارد" + + notice_folder_created: پوشه ساخته شد + error_folder_creation_failed: ساخت پوشه با شکست مواجه شد + error_folder_title_must_be_entered: عنوان باید وارد شده باشد + notice_folder_deleted: پوشه حذف شد + error_folder_title_is_already_used: عنوان قبلا استفاده شده است + notice_folder_details_were_saved: جزئیات پوشه ذخیره شد + error_folder_is_locked: پوشه قفل است + error_file_is_locked: پرونده قفل است + notice_file_deleted: پرونده حذف شد + error_at_least_one_revision_must_be_present: حداقل یک بازبینی باید موجود باشد + notice_revision_deleted: بازبینی حذف شد + notice_revision_obsoleted: بازبینی منسوخ شد + warning_one_of_files_locked: یکی از پرونده‌ها قفل است + notice_file_revision_created: بازبینیِ پرونده ساخته شد + notice_your_preferences_were_saved: تنظیمات ذخیره شد + notice_your_preferences_were_not_saved: تنظیمات ذخیره نشد + warning_folder_notifications_already_activated: آگاه‌سازی پوشه قبلا فعال شده است + + notice_folder_notifications_activated: آگاه‌سازی پوشه فعال شد + warning_folder_notifications_already_deactivated: آگاه‌سازی پوشه قبلا غیرفعال شده است + + notice_folder_notifications_deactivated: آگاه‌سازی پوشه غیرفعال شد + warning_file_notifications_already_activated: آگاه‌سازی پرونده قبلا فعال شده است + + notice_file_notifications_activated: آگاه‌سازی پرونده فعال شد + warning_file_notifications_already_deactivated: آگاه‌سازی پرونده قبلا غیرفعال شده است + + notice_file_notifications_deactivated: آگاه‌سازی پرونده غیرفعال شد + link_details: "جزئیات %{title}" + link_edit: "ویرایش %{title}" + link_create_folder: ساخت پوشه + link_title: عنوان + link_size: اندازه + link_modified: آخرین تغییر + link_ver: نسخه + link_author: نویسنده + title_check_for_zip_download_or_email: انتخاب کنید؛ برای فشرده کردن، دریافت یا رایانامه + title_check_for_restore_or_delete: انتخاب کنید برای بازیابی یا حذف + + title_notifications_active_deactivate: "آگاه‌سازی‌ها فعال است؛ برای غیرفعال کردن کلیک کنید" + title_notifications_not_active_activate: "آگاه‌سازی‌ها غیرفعال است؛ برای فعال کردن کلیک کنید" + title_title_version_version_download: "%{title} نسخه %{version} دریافت" + title_locked_by_user: "توسط %{user} قفل شده" + title_waiting_for_approval: منتظر تایید + title_approved: تایید شده + title_unlock_file: برای اجازه تغییر به سایر کاربران، قفل را باز کنید + title_lock_file: برای جلوگیری از تغییرات سایر کاربران، قفل کنید + title_download_checked: دریافت در قالب پروند فشرده + title_send_checked_by_email: ارسال توسط رایانامه + link_user_preferences: تنظیمات اسناد پیش‌رفته + heading_send_documents_by_email: ارسال اسناد توسط رایانامه + label_email_from: از + label_email_to: به + label_email_cc: رونوشت + label_email_subject: موضوع + label_email_documents: اسناد + label_email_body: متن رایانامه + label_email_send: ارسال + title_notifications_active: آگاه‌سازی‌ها فعال + label_upload: بارگذاری + heading_new_folder: پوشه جدید + label_title: عنوان + label_description: توضیحات + submit_save: ذخیره + info_file_locked: پرونده قفل شده! + label_notifications: آگاه‌سازی‌ها + select_option_default: پیش‌فرض + select_option_deactivated: غیرفعال + select_option_activated: فعال + label_title_format: قالبِ عنوان + text_title_format: "قالب عنوانِ پرونده، هنگام دریافت (%t - عنوان, %f - file, %d - date, %v - بازبینی, %i - ID, %r - revision). مثال: %t_%v" + title_save_preferences: ذخیره تنظیمات + heading_revisions: بازبینی‌ها + title_download: دریافت + title_delete_revision: حذف بازبینی + title_obsolete_revision: بازبینی منسوخ + label_created: ساخته شده در + label_changed: تغییر یافته + info_changed_by_user: "%{changed} توسط" + label_filename: نام پرونده + label_mime: MIME + label_size: اندازه + heading_new_revision: بازبینی جدید + option_version_same: مشابه + option_version_patch: وصله + option_version_minor: مینور + option_version_major: ماژور + option_version_custom: دلخواه + label_new_content: محتوای جدید + label_maximum_files_download: حداکثر تعداد پرونده‌ها برای دریافت + note_maximum_number_of_files_downloaded: تعداد پرونده‌هایی که به صورت هم‌زمان می‌توان دریافت کرد. 0 به معنی نامحدود است. + label_file_storage_directory: مسیر ذخیره‌سازی پرونده‌ها + label_index_database: پایگاه داده نمایه + label_stemming_language: زبان مورد استفاده برای نمایه‌گذاری + note_possible_values: مقادیر ممکن + note_pass_none_to_disable_stemming: "مقدار 'هیچ' برای غیرفعال کردن ریشه‌یابی" + label_stem_strategy: راه‌بر + option_stem_none: ریشه‌یابی هیچ (پیش‌فرض) + option_stem_some: ریشه‌یابی بعضی + option_stem_all: ریشه‌یابی همه + text_stemming_info: "این گزینه نحوه‌ی اعمال ریشه‌یابی توسط تجزیه‌کننده جستار را مشخص می‌کند. مقدار پیش‌فرض «ریشه‌یابی هیچ» است. گزینه‌های قابل انتخاب: STEM_NONE - هیچ ریشه‌یابی‌ای انجام نمی‌شود, STEM_SOME - ریشه‌یابی را روی کلمات انجام می‌دهد مگر کلماتی که با حروف بزرگ شروع شده‌اند یا به یکی از این حروف منتهی می‌شوند.(در حال حاضر:'/@<>=*[{\"'), یا با عمل‌گرهایی استفاده شده‌اند که نیازمند اطلاعات موقعیت کاربرد هستند. کلمات مشتق شده با پیش‌وند 'Z' مشخص می‌شوند. , STEM_ALL - جستجو برای همه انواع مشتقات کلمه (نکته: پیش‌وند 'Z' اضافه نمی‌شود)." + label_default_notifications: آگاه‌سازی‌های پیش‌فرض پرونده‌ها + heading_uploaded_files: پرونده‌های بارگذاری شده + link_documents: اسناد + permission_view_dmsf_file_revision_accesses: مشاهده دریافت‌ها در «خط زمان» + permission_view_dmsf_file_revisions: مشاهده بازبینی‌ها در «خط زمان» + permission_view_dmsf_folders: مرور اسناد + permission_user_preferences: ترجیحات کاربر + permission_view_dmsf_files: مشاهده اسناد + permission_folder_manipulation: دستکاری پوشه + permission_file_manipulation: دستکاری پرونده + permission_force_file_unlock: بازکردن اجباری قفل پرونده + permission_manage_workflows: مدیریت گردش اسناد + permission_file_delete: حذف اسناد + permission_display_system_folders: نمایش پوشه‌های سیستم + permission_file_approval: تایید پرونده + permission_email_documents: ارسال اسناد با رایانامه + label_file: پرونده + field_folder: پوشه + error_file_commit_require_uploaded_file: ثبت پرونده، نیازمند پرونده‌ی بارگذاری شده است + + warning_some_files_were_not_committed: "بعضی از پرونده‌ها به دلیل خطای اعتبارسنجی بارگذاری نشدند: %{files}" + + error_user_has_not_right_delete_folder: "کاربر دسترسی حذف پوشه را ندارد" + + error_user_has_not_right_delete_file: "کاربر دسترسی حذف پرونده را ندارد" + + notice_entries_deleted: موارد پاک شده + warning_some_entries_were_not_deleted: "بعضی موارد حذف نشده‌اند': %{entries}" + title_delete_checked: حذف + title_items: موراد + title_filename_for_download: نام پرونده مورد استفاده برای دریافت یا داخل فایل فشرده + label_number_of_folders: پوشه + label_number_of_documents: مستند + error_file_storage_directory_does_not_exist: "پوشه محل ذخیره‌سازی پرونده وجود ندارد و نمی‌تواند ساخته شود." + + error_file_can_not_be_created: "پرونده‌ها نمی‌توانند در پوشه‌ی ذخیره‌سازی ساخته شوند." + error_wrong_zip_encoding: رمزگذاری اشتباه zip + warning_xapian_not_available: Xapian در دسترس نیست + menu_dmsf: اسناد پیش‌رفته # Project tab title + label_physical_file_delete: پرونده فیزیکی حذف شود + user_is_not_project_member: شما عضو این پروژه نیستید + heading_access_downloads_emails: دریافت‌ها/رایانامه‌ها + heading_access_first: اول + heading_access_last: آخر + label_dmsf_updated: به‌روزرسانی شد + label_dmsf_downloaded: دریافت شد + title_total_size_of_all_files: مجموع اندازه‌های پرونده‌های این پوشه + project_module_dmsf: اسناد پیش‌رفته # Project module name + warning_no_project_to_copy_file_to: پروژه‌ای برای رونوشت پرونده‌ها نیست + comment_copied_from: "رونوشت از %{source}" + field_target_project: پروژه هدف + field_target_folder: پوشه هدف + title_copy_or_move: رونوشت/انتقال + label_dmsf_folder_plural: پوشه‌های اسناد پیش‌رفته # Search options + comment_moved_from: "منتقل شده از %{source}" + error_target_folder_same: پوشه مقصد و پروژه مشابه فعلی هستند. + title_copy: رونوشت + + error_max_email_filesize_exceeded: "شما از بیش‌ترین اندازه مجاز پرونده برای ارسال توسط رایانامه فراتر رفته‌اید. (%{number} مگابایت)" + + note_maximum_email_filesize: بیش‌ترین اندازه پرونده برای ارسال توسط رایانامه را محدود می‌کند. صفر به معنی نامحدود. (واحد مگابایت). + + label_maximum_email_filesize: بیش‌ترین اندازه پیوست رایانامه + header_minimum_filesize: خطای پرونده. + error_minimum_filesize: "پرونده %{file} صفر بایت است و پیوست نخواهد شد." + parent_directory: پوشه والد + note_webdav: "Webdav وقتی فعال شود می‌توان آن‌را در %{protocol}://%{domain}/dmsf/webdav/[project identifier] پیدا کرد" + + label_copy_dmsf: "رونوشت پرونده‌ها و پوشه‌ها (%{files} پرونده در %{folders} پوشه)" + label_copy_only_dmsf_folders: "فقط رونوشت از پوشه‌ها (%{folders})" + + warning_folder_already_locked: این پوشه قبلا قفل شده است + notice_folder_locked: پوشه با موفقیت قفل شد + warning_folder_not_locked: متاسفانه این پوشه نمی‌تواند قفل شود + notice_folder_unlocked: قفل پوشه با موفقیت باز شد + error_only_user_that_locked_folder_can_unlock_it: شما اجازه‌ی باز کردن قفل این پوشه را ندارید + + title_unlock_folder: برای امکان تغییر توسط سایرین، قفل پوشه را باز کنید + title_lock_folder: برای جلوگیری از تغییر توسط سایرین، پوشه را قفل کنید + + select_option_webdav_readonly: فقط خواندنی + select_option_webdav_readwrite: خواندن/نوشتن + label_webdav_strategy: عمل‌کرد WebDAV + + note_webdav_strategy: امکان تعیین این‌که WebDav به صورت فقط خواندنی کار کند یا نوشتن/خواندن را به راه‌بر می‌دهد. + + error_unable_delete_dmsf_workflow: حذف روال تایید امکان‌پذیر نیست + error_empty_note: "یادداشت نمی‌تواند خالی باشد" + error_workflow_assign: در هنگام تخصیص، خطایی رخ داد + error_cannot_start_workflow: "روال تایید نمی‌تواند شروع شود" + error_cannot_renumber_steps: "گام‌ها نمی‌توانند دوباره شماره‌گذاری شوند" + label_dmsf_workflow_new: روال تایید جدید + field_label_dmsf_workflow: روال تایید + field_label_dmsf_workflow_name: نام روال تایید + label_dmsf_workflow_plural: روال‌های تایید + label_dmsf_workflow_plural_num: روال تایید (%{count}) + label_dmsf_workflow_step: گام + label_dmsf_workflow_step_plural: گام‌ها + label_dmsf_workflow_approval_plural: تایید + label_dmsf_wokflow_action_approve: تایید + label_dmsf_wokflow_action_reject: رد + label_dmsf_wokflow_action_delegate: واگذار به + label_dmsf_wokflow_action_assign: یک روال تایید تخصیص دهید + label_dmsf_wokflow_action_start: شروع روال تایید + label_dmsf_workflow_add_approver: "یک تایید کننده جدید با یک کارکرد منطقی اضافه کنید:" + label_or: یا + label_action: عملیات + label_note: یادداشت + title_none: هیچ + title_rejection: رد + title_delegation: صدور + title_assignment: تخصیص + title_start: شروع + title_dmsf_workflow_log: سابقه روال تایید + title_assigned: تخصیص داده شده + title_approval: تایید + title_rejected: رد شده + title_obsolete: منسوخ + dmsf_and: و + dmsf_or: یا + dmsf_new_step: گام جدید + dmsf_new_step_or_approver: گام یا تایید کننده جدید + message_dmsf_wokflow_note: یادداشت شما ... + info_revision: "r %{rev}" + link_workflow: روال تایید + notice_workflow_started: روال تایید با موفقت شروع شد + text_email_subject_approved: تایید شده + text_email_subject_rejected: رد شده + text_email_subject_delegated: صادر شده + text_email_subject_requires_approval: نیازمند تایید شماست + text_email_subject_updated: به‌روزرسانی شده + text_email_subject_started: شروع شده + text_email_finished_approved: "روال تایید '%{name}' که به '%{filename}' اختصاص داده شده بود پایان یافته است و سند تایید شده است." + text_email_finished_rejected: "روال تایید '%{name}' که به '%{filename}' اختصاص داده شده بود پایان یافته است و سند به دلیل '%{notice}' رد شده است." + text_email_finished_delegated: "روال تایید '%{name}' که به پرونده '%{filename}' اختصاص یافته است، به دلیل '%{notice}' به شما واگذار شده و در این مرحله نیازمند تایید شماست." + text_email_finished_step: "روال تایید '%{name}' که به پرونده '%{filename}' اختصاص یافته است، یکی از مراحل تایید را گذرانده و در این مرحله نیازمند تایید شماست" + text_email_finished_step_short: "روال تایید '%{name}' که به پرونده '%{filename}' اختصاص یافته است، یکی از مراحل تایید را گذرانده است." + text_email_started: "روال تایید '%{name}' که به پرونده '%{filename}' اختصاص یافته است، شروع شده و در این مرحله نیازمند تایید شماست." + text_email_to_proceed: برای شروع/ادامه روی شکلک «تیک» که بعد از سند نمایش داده شده است کلیک کنید + text_email_to_see_history: برای مشاهده سابقه روال تایید، روی وضعیت تایید سند کلیک کنید + + text_email_to_see_status: برای مشاهده وضعیت فعلی روال تایید، روی وضعیت تایید سند کلیک کنید + + title_create_link: یک پیوند بسازید + label_link_from: پیوند از + label_link_to: پیوند به + label_notifications_on: فعال کردن آگاه‌سازی‌ها + label_notifications_off: غیرفعال کردن آگاه‌سازی‌ها + field_target_file: پرونده منبع + title_download_entries: گزارش دریافت‌ها + label_external: بیرونی + label_internal: داخلی + + label_link_name: نام پیوند + field_external_url: نشانی + label_target_folder: پوشه مقصد + label_source_folder: پوشه مبدا + label_target_project: پروژه مقصد + label_source_project: پروژه مبدا + + text_email_doc_updated_subject: اسناد به‌روزرسانی شدند + text_email_doc_updated: مستندات را ویرایش کرده است + text_email_doc_follows: به شرح زیر + text_email_doc_deleted_subject: اسناد حذف شدند + text_email_doc_deleted: مستندات را حذف کرده است + label_links_only: فقط پیوندها + + label_display_notified_recipients: نمایش گیرندگان آگاه‌سازی + note_display_notified_recipients: کاربر از تمامی کاربرانی که آگاه‌سازی را دریافت کرده‌اند مطلع می‌شود. + + warning_email_notifications: "رایانامه‌ی آگاه‌سازی به %{to} ارسال شد" + + link_trash_bin: سطل بازیافت + title_restore: بازیابی + notice_dmsf_file_restored: پرونده با موفقیت بازیابی شد + notice_dmsf_folder_restored: پوشه با موفقیت بازیابی شد + notice_dmsf_link_restored: پیوند با موفقیت بازیابی شد + title_restore_checked: بازیابی + error_parent_folder: "پوشه والد وجود ندارد" + + error_resource_or_parent_locked: نمی‌توان قفل کرد - منبع (یا والدش) قفل هستند + error_parent_locked: نمی‌توان قفل کرد - والد منبع، قفل است + error_resource_locked: نمی‌توان قفل کرد - منبع، قفل است + error_lock_exclusively: امکان قفل انحصاری یک منبعِ قفل شده، وجود ندارد + error_unlock_parent_locked: شکست در باز کردن قفل - والد قفل است + + label_dmsf_version: نسخه + + locked_documents: اسناد قفل شده + open_approvals: تاییدهای باز + watched_documents: مستندات تحت نظارت + + error_maximum_upload_filecount: "بیش از %{filecount} فایل نمی‌تواند بارگذاری شود." + + label_public_urls: نشانی‌های عمومی تا این تاریخ معتبرند + + label_webdav: Webdav عمل‌کرد + label_full_text: جستجوی تمام-متن + link_extension: پسوند + + label_webdav_ignore: الگوهای فایل‌های مستثنی + note_webdav_ignore: یک عبارت منظم روی نام فایل‌ها که در دستور PUT نادیده گرفته شوند. + + label_document_url: نشانی + label_last_revision_id: بازبینی + + label_webdav_disable_versioning: هیچ الگویی برای غیرفعال کردن نسخه‌بندی نیست + note_webdav_disable_versioning: یک عبارت منظم که بر اساس آن مدیریت نسخه‌ها برای پرونده‌های تطبیق یافته غیرفعال می‌شود. الگوی پیش‌فرض با پرونده‌های موقتی که توسط MsOffice ساخته می‌شوند تطبیق می‌یابد. + + label_dmsf_keep_documents_locked: اسناد را قفل نگه دار + note_dmsf_keep_documents_locked: اسناد وقتی تایید شوند قفل می‌مانند + note_global: (سراسری) + field_dmsf_not_inheritable: موروثی نیست + + label_webdav_use_project_names: استفاده از عنوان پروژه برای نام پوشه + note_webdav_use_project_names: استفاده از عنوان پروژه به جای شناسه‌ی پروژه‌ها برای نام پوشه‌ها. + + label_last_approver: آخرین تایید کننده + + label_act_as_attachable: فعالیت به عنوان پیوست‌پذیر + note_dmsf_act_as_attachable: اجازه می‌دهد که به اشیاء دیگر (مثل مسأله‌ها) پیوست شود + + label_user_search_add: جستجوی کاربر برای اضافه کردن + + label_dmsf_attachments: پیوست‌های پیش‌رفته + label_basic_attachments: پیوست‌های پایه + + label_email_from_override: از + text_email_from_override: کاربر در سامانه است + label_email_reply_to: ارسال پاسخ به + + label_enable_cjk_ngrams: فعال‌سازی ایجاد n-gram از متن CJK + text_enable_cjk_ngrams: "با فعال‌سازی این قسمت، نویسه‌های CJK به تک-نویسه و دو-نویسه تقسیم می‌شوند و تک-نویسه‌ها حامل اطلاعات مکانی نیز می‌باشند. نویسه‌های غیرCJK بصورت عادی به کلمات تقسیم می‌شوند." + + label_dmsf_fast_links: پیوند‌های سریع + text_dmsf_fast_links_info: شما قادر خواهید بود که بصورت دستی شناسه پوشه مقصد را هنگامی که در حال ایجاد پیوند هستید، وارد کنید. این کار سرعت فرآیند ایجاد پیوند را تسریع می کند + + label_dmsf_permissions: اجازه دسترسی فقط به + label_inherited_permissions: دسترسی‌های ارث‌بری شده + + button_edit_content: ویرایش محتوا + field_workflow: روال تایید + field_modified: تاریخ تغییر + field_updated: به‌روزرسانی + field_count: دریافت + field_first_at: اولین + field_last_at: آخرین + field_size: حجم + field_locked: Locked + + label_add_width: اضافه با + + dmsf_webdav_ignore_1b_file_for_authentication: نادیده گرفتن فایل‌های ۱بایتی که برای احراز هویت ارسال می‌شوند + dmsf_webdav_ignore_1b_file_for_authentication_info: افزونه WebDAV برای Total Commander + + text_not_empty: پوشه خالی نیست. + label_scroll_down: رفتن به پایین + note_webdav_disabled: WebDAV غیرفعال است. با راه‌بر مطرح کنید. + + dmsf_copy: "رونوشت (%{n})" + label_empty_trash_bin: خالی کردن + label_dmsf_projects_as_subfolders: زیرپروژه‌ها به شکل زیرشاخه‌ها + note_dmsf_projects_as_subfolders: افزودن زیرپروژه‌ها به شکل زیرشاخه در اسناد + only_approval_zero_minor_version: فقط مینور ورژن صفر امکان تخصیص روال تایید دارد + title_assignment_minor: تخصیص مجاز نیست، مینور باید صفر باشد. + title_start_minor: شروع غیر مجاز است، مینور باید صفر باشد. + title_approval_minor: تایید مجاز نیست، مینور باید صفر باشد. + + label_project_watchers: ناظرها + label_dmsf_folder_watchers: ناظرها + label_dmsf_file_watchers: ناظرها + label_dmsf_watched: مستندات تحت نظارت + dmsf_legacy_notifications: آگاه‌سازی قدیمی + permission_view_dmsf_folder_watchers: مشاهده ناظرهای پوشه + permission_add_dmsf_folder_watchers: افزونه ناظر پوشه + permission_delete_dmsf_folder_watchers: حذف ناظر پوشه + permission_view_dmsf_file_watchers: مشاهده ناظرهای سند + permission_add_dmsf_file_watchers: افزودن ناظر سند + permission_delete_dmsf_file_watchers: حذف ناظر سند + permission_view_project_watchers: مشاهده ناظرهای پروژه + permission_add_project_watchers: افزودن ناظر پروژه + permission_delete_project_watchers: حذف ناظر پروژه + label_dmsf_new_top_level_document: سند جدید در بالاترین سطح + label_dmsf_new_top_level_folder: پوشه جدید در بالاترین سطح + + label_dmsf_max_notification_receivers_info: بیش‌ترین تعداد نمایش دریافت‌کننده‌‌های آگاه‌سازی + note_dmsf_max_notification_receivers_info: تعداد نمایش داده شده از کسانی که آگاه‌سازی را دریافت می‌کنند محدود می‌کند. + label_dmsf_office_bin: فایل اجرایی Libreoffice + note_dmsf_office_bin: یک برنامه برای تبدیل اسناد به PDF و پیش‌نمایش آن‌هاست. اگر می‌خواهید جلوی ایجاد پیش‌نمایش را بگیرید این مقدار را خالی بگذارید. + + note_dmsf_office_bin_not_available: "دستور '%{value}' در دسترس نیست" + + label_column_id: شناسه + label_column_title: عنوان + label_column_size: حجم + label_column_modified: تاریخ به‌روزرسانی + label_column_version: نسخه + label_column_workflow: روال تایید + label_column_author: نویسنده + label_column_description: توضیحات + label_column_comment: توضیح + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: شامل نویسه‌های غیرمجاز است diff --git a/config/locales/fr.yml b/config/locales/fr.yml new file mode 100644 index 00000000..d0a46667 --- /dev/null +++ b/config/locales/fr.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Atmis +# +# 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 +# . + +fr: + dmsf: DMS # Custom fields t title + label_dmsf_file: DMS Fichier + label_dmsf_file_plural: DMS Fichiers # Email subject & Search options + label_dmsf_file_revision_plural: Révisions du document + label_dmsf_file_revision_access_plural: Accès au document + warning_no_entries_selected: Aucun fichier sélectionné + error_email_to_must_be_entered: "La saisie d'une adresse mail est obligatoire" + warning_file_already_locked: Fichier déjà verrouillé + notice_file_locked: Fichier verrouillé + warning_file_not_locked: Fichier déverrouillé + notice_file_unlocked: Fichier déverrouillé + error_only_user_that_locked_file_can_unlock_it: "Le fichier ne peut être déverrouillé que par celui qui l'a + verrouillé" + error_max_files_exceeded: Le nombre de fichiers pouvant être téléchargés simultanément est dépassé + error_entry_project_does_not_match_current_project: "Le projet saisi ne correspond pas au projet courant" + + notice_folder_created: Dossier créé + error_folder_creation_failed: Erreur de création du dossier + error_folder_title_must_be_entered: Le titre du document est requis + notice_folder_deleted: Dossier supprimé + error_folder_title_is_already_used: Le titre du fichier est déjà utilisé + notice_folder_details_were_saved: Les détails du dossier ont été enregistrés + error_folder_is_locked: Le dossier est verrouillé + error_file_is_locked: Le fichier est verrouillé + notice_file_deleted: Le fichier a été supprimé + error_at_least_one_revision_must_be_present: Au moins une révision est requise + notice_revision_deleted: Révision supprimée + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: Un des fichiers sélectionnés est verrouillé + notice_file_revision_created: La révision du fichier a été ajoutée + notice_your_preferences_were_saved: Vos paramètres ont été enregistrés + notice_your_preferences_were_not_saved: Your preferences were not saved + warning_folder_notifications_already_activated: Les notifications du dossier sont déjà activées + + notice_folder_notifications_activated: Les notifications du dossier ont été activées + warning_folder_notifications_already_deactivated: Les notifications du dossier sont déjà désactivées + + notice_folder_notifications_deactivated: Les notifications des dossier ont été désactivés + warning_file_notifications_already_activated: Les notifications du fichier sont déjà désactivés + + notice_file_notifications_activated: Les notifications du fichier ont été activées + warning_file_notifications_already_deactivated: Les notifications du fichier sont déjà désactivées + + notice_file_notifications_deactivated: Les notifications du fichier ont été désactivés + link_details: "Détails de %{title}" + link_edit: "Modification du dossier %{title}" + link_create_folder: Créer un sous-dossier + link_title: Titre + link_size: Taille + link_modified: Modifié + link_ver: Version + link_author: Auteur + title_check_for_zip_download_or_email: Sélectionner pour le téléchargement ou la transmission par mail + title_check_for_restore_or_delete: Sélectionner pour restaurer ou supprimer + + title_notifications_active_deactivate: "Notifications activées : cliquer pour désactiver" + title_notifications_not_active_activate: "Notifications désactivées : cliquer pour activer" + title_title_version_version_download: "Télécharger la version %{version} de %{title}" + title_locked_by_user: "Verrouillé par %{user}" + title_waiting_for_approval: Attente de validation + title_approved: Validé + title_unlock_file: Déverrouiller afin de permettre la modification par les membres du projet + title_lock_file: "Verrouiller afin d'empêcher les modifications du document" + title_download_checked: Télécharger les fichiers sélectionnés au format zip + title_send_checked_by_email: Transmettre les fichiers sélectionnés par mail + link_user_preferences: Préférences personnelles du module DMS + heading_send_documents_by_email: Transmettre les documents par mail + label_email_from: De + label_email_to: A + label_email_cc: Cc + label_email_subject: Objet + label_email_documents: Fichiers + label_email_body: Message + label_email_send: Envoyer + title_notifications_active: Notifications actives + label_upload: Transmission + heading_new_folder: Nouveau Dossier + label_title: Titre + label_description: Description + submit_save: Enregistrer + info_file_locked: Fichier verrouillé + label_notifications: Notifications + select_option_default: Défaut + select_option_deactivated: Désactivé + select_option_activated: Activé + label_title_format: Format du titre + text_title_format: "Format du titre du document pour le téléchargement (%t - titre, %f - fichier, %d - date, %v - + version, %i - ID, %r - révision). Example: %t_%v" + title_save_preferences: Enregistrer les préférences + heading_revisions: Révisions + title_download: Télécharger + title_delete_revision: Supprimer la révision + title_obsolete_revision: Obsolete revision + label_created: Créé + label_changed: Modifié + info_changed_by_user: "%{changed} par" + label_filename: Fichier + label_mime: Type + label_size: Taille + heading_new_revision: Nouvelle révision + option_version_same: (identique) + option_version_patch: Patch + option_version_minor: (modification mineure) + option_version_major: (modification majeure) + option_version_custom: (personnalisée) + label_new_content: Nouvelle version du fichier + label_maximum_files_download: Nombre maximal de fichiers pouvant être téléchargés + note_maximum_number_of_files_downloaded: Nombre maximal de documents pouvant être téléchargés ou transmis par mail en + une fois. La valeur 0 signifie illimité. + label_file_storage_directory: Dossier de stockage des documents + label_index_database: Indexer la base de données + label_stemming_language: Méthode de racinisation + note_possible_values: valeurs possibles + note_pass_none_to_disable_stemming: "Utiliser 'none' pour désactiver la racinisation" + label_stem_strategy: Stratégie de racinisation + option_stem_none: Aucun suffixe(défaut) + option_stem_some: Quelques suffixes + option_stem_all: Tous les suffixes + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: Notifications par défaut du document + heading_uploaded_files: Document(s) transmis + link_documents: Documents + permission_view_dmsf_file_revision_accesses: Voir les téléchargements dans le flux d'activité + permission_view_dmsf_file_revisions: Voir les révisions dans le flux d'activité + permission_view_dmsf_folders: Parcourir les documents + permission_user_preferences: Préférences utilisateur + permission_view_dmsf_files: Afficher documents + permission_folder_manipulation: Gestion des dossiers + permission_file_manipulation: Gestion des documents + permission_force_file_unlock: Forcer le déverrouillage du document + permission_manage_workflows: Gérer les flux de validation + permission_file_delete: Supprimer les documents + permission_display_system_folders: Afficher les dossiers système + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: Fichier + field_folder: Dossier + error_file_commit_require_uploaded_file: "Transmission des fichiers nécessaire avant l'enregistrement" + + warning_some_files_were_not_committed: "Erreur d'enregsitrement de certains fichiers %{files}" + + error_user_has_not_right_delete_folder: "L'utilisateur ne dispose pas des droits nécessaires permettant la suppression + du dossier" + error_user_has_not_right_delete_file: "L'utilisateur ne dispose pas des droits nécessaires permettant la suppression + du dossier" + notice_entries_deleted: Elément(s) supprimé(s) + warning_some_entries_were_not_deleted: "Certains éléments n'ont pas été supprimés : %{entries}" + title_delete_checked: Supprimer les éléments sélectionnés + title_items: éléments + title_filename_for_download: "Nom du fichier à utiliser lors du téléchargement ou de l'archive ZIP" + label_number_of_folders: Dossiers + label_number_of_documents: Fichiers + error_file_storage_directory_does_not_exist: Le répertoire de stockage des fichiers n'existe pas ou n'a pas pu être + créé + error_file_can_not_be_created: "Le fichier n'a pas pu être enregistré dans le répertoire de stockage" + error_wrong_zip_encoding: Mauvais jeu de caractères pour la transformation du nom du ZIP + warning_xapian_not_available: Le module Xapian est indisponible + menu_dmsf: DMS # Project tab title + label_physical_file_delete: Suppression des fichiers + user_is_not_project_member: "Vous n'êtes pas un membre du projet" + heading_access_downloads_emails: Téléchargement / Envoi par mail + heading_access_first: Premier + heading_access_last: Dernier + label_dmsf_updated: Dépôt + label_dmsf_downloaded: Téléchargé + title_total_size_of_all_files: Taille totale des fichiers de ce dossier + project_module_dmsf: DMS # Project module name + warning_no_project_to_copy_file_to: "Le projet de destination n'est pas défini" + comment_copied_from: "Copie effectuée depuis %{source}" + field_target_project: Projet cible + field_target_folder: Dossier cible + title_copy_or_move: Copie/Déplacement + label_dmsf_folder_plural: DMS Les dossiers # Search options + comment_moved_from: "Déplacé depuis %{source}" + error_target_folder_same: Le projet et le dossier cible sont identiques au projet et dossier source + title_copy: Copie + + error_max_email_filesize_exceeded: "Vous avez dépassé la taille maximale des fichiers pouvant être transmis par mail + (%{number} MB)" + note_maximum_email_filesize: Taille maximale, en méga octets, des fichiers pouvant être transmis par mail. 0 indique + aucune restriction + label_maximum_email_filesize: Taille maximale du fichier attaché + header_minimum_filesize: Erreur de fichier. + error_minimum_filesize: "Le fichier %{file} est vide. Il ne sera pas transmis." + parent_directory: Dossier parent + note_webdav: "Après l'activation du module Webdav, celui-ci sera accessible par + %{protocol}://%{domain}/dmsf/webdav/[project identifier]" + label_copy_dmsf: "Copier les fichiers et les dossiers (%{files} fichiers dans %{folders} dossiers)" + label_copy_only_dmsf_folders: "Copier que les dossiers (%{folders})" + + warning_folder_already_locked: Ce dossier est déjà verrouillé + notice_folder_locked: Dossier verrouillé + warning_folder_not_locked: Echec du verrouillage du dossier + notice_folder_unlocked: Le dossier a été déverrouillé + error_only_user_that_locked_folder_can_unlock_it: "Vous n'êtes autorisé à déverrouiller ce dossier" + + title_unlock_folder: Déverrouiller afin de permettre la modification par les membres du projet + title_lock_folder: "Verrouiller afin d'empêcher les modifications du dossier" + + select_option_webdav_readonly: Lecture + select_option_webdav_readwrite: Lecture/Écriture + label_webdav_strategy: Accès Webdav + + note_webdav_strategy: "Permet à l'administrateur d'autoriser les utilisateurs au module Webdav en letcure seule ou en + lecture et écriture." + + error_unable_delete_dmsf_workflow: Impossible de supprimer le flux de validation + error_empty_note: La note ne peut pas être vide + error_workflow_assign: "Une erreur s'est produite lors de l'attribution" + error_cannot_start_workflow: Le flux de validation ne peut pas être démarré + error_cannot_renumber_steps: Les étapes ne peuvent pas être renumérotées + label_dmsf_workflow_new: Nouveau flux de validation + field_label_dmsf_workflow: Flux de validation + field_label_dmsf_workflow_name: Nom du flux + label_dmsf_workflow_plural: Flux de validation + label_dmsf_workflow_plural_num: Flux de validation (%{count}) + label_dmsf_workflow_step: Etape + label_dmsf_workflow_step_plural: Etapes + label_dmsf_workflow_approval_plural: Approbations + label_dmsf_wokflow_action_approve: Approuver + label_dmsf_wokflow_action_reject: Rejeter + label_dmsf_wokflow_action_delegate: Déléguer à + label_dmsf_wokflow_action_assign: Attribuer un flux de validation + label_dmsf_wokflow_action_start: Démarrer le flux de validation + label_dmsf_workflow_add_approver: "Ajouter un nouvel approbateur avec une fonction logique :" + label_or: ou + label_action: Action + label_note: Note + title_none: Aucun + title_rejection: Rejet + title_delegation: Délégation + title_assignment: Attribution + title_start: Démarrer + title_dmsf_workflow_log: Journal du flux de validation + title_assigned: Assigné + title_approval: Approbation + title_rejected: Rejeté + title_obsolete: Obsolete + dmsf_and: ET + dmsf_or: OU + dmsf_new_step: Nouvelle étape + dmsf_new_step_or_approver: Nouvelle étape ou Nouvel approbateur + message_dmsf_wokflow_note: Votre note... + info_revision: "r %{rev}" + link_workflow: Flux + notice_workflow_started: Flux de validation démarré avec succès + text_email_subject_approved: approuvé + text_email_subject_rejected: rejeté + text_email_subject_delegated: délégué + text_email_subject_requires_approval: requiert votre approbation + text_email_subject_updated: mis à jour + text_email_subject_started: démarré + text_email_finished_approved: "Le flux de validation '%{name}' assigné au document '%{filename}' vient de se terminer + et le document a été approuvé." + text_email_finished_rejected: "Le flux de validation '%{name}' assigné au document '%{filename}' vient de se terminer + et le document a été rejeté pour la raison '%{notice}'." + text_email_finished_delegated: "Le flux de validation '%{name}' assigné au document '%{filename}' a été délégué pour + la raison '%{notice}' et vous êtes tenu d'approuver l'étape actuelle '%{stepname}'." + text_email_finished_step: "Le flux de validation '%{name}' assigné au document '%{filename}' a passé une des étapes + d'approbation et vous êtes tenu d'approuver l'étape suivante." + text_email_finished_step_short: "Le flux de validation '%{name}' assigné au document '%{filename}' a passé une des + étapes d'approbation." + text_email_started: "Le flux de validation '%{name}' assigné au document '%{filename}' vient de démarrer et vous êtes + tenu d'approuver l'étape actuelle '%{stepname}'." + text_email_to_proceed: "Pour ce faire, cliquez sur l'icône de validation à côté du document dans" + text_email_to_see_history: "Pour consulter l'historique de validation, cliquez sur le statut du flux du document dans" + + text_email_to_see_status: Pour consulter le statut actuel du flux de validation, cliquez sur le statut du flux du + document dans + + title_create_link: Créer un lien symbolique + label_link_from: Lien depuis + label_link_to: Lien vers + label_notifications_on: Activer les notifications + label_notifications_off: Désactiver les notifications + field_target_file: Fichier source + title_download_entries: Historique des téléchargements + label_external: Externe + label_internal: Internal + + label_link_name: Nom du lien + field_external_url: Adresse Internet + label_target_folder: Dossier cible + label_source_folder: Dossier source + label_target_project: Projet cible + label_source_project: Projet source + + text_email_doc_updated_subject: Documents mis à jour + text_email_doc_updated: a mis à jour des documents de + text_email_doc_follows: comme suit + text_email_doc_deleted_subject: Documents supprimés + text_email_doc_deleted: a supprimé des documents de + label_links_only: liens seulement + + label_display_notified_recipients: Afficher les destinataires notifiés + note_display_notified_recipients: "L'utilisateur sera informé de tous les destinataires à qui un email de notifcation + a été envoyé" + warning_email_notifications: "Email de notification envoyé à %{to}" + + link_trash_bin: Corbeille + title_restore: Récupérer + notice_dmsf_file_restored: Le document a été récupéré avec succès + notice_dmsf_folder_restored: Le dossier a été récupéré avec succès + notice_dmsf_link_restored: Le lien a été récupéré avec succès + title_restore_checked: Restauration vérifiée + error_parent_folder: "Le dossier parent n'existe pas" + + error_resource_or_parent_locked: Impossible de verrouiller - la ressource (ou parent) est verrouillée + error_parent_locked: Impossible de verrouiller - la ressource parente est verrouillée + error_resource_locked: Impossible de verrouiller - la ressource est verrouillée + error_lock_exclusively: Impossible de verrouiller de manière exclusive une ressource déjà verrouillée + error_unlock_parent_locked: Echec du déverrouillage - la ressource parente est verrouillée + + label_dmsf_version: Version + + locked_documents: Documents verrouillés + open_approvals: Approbations en attente + watched_documents: Watched documents + + error_maximum_upload_filecount: "Pas plus de %{filecount} fichier(s) ne peuvent être téléversés." + + label_public_urls: URLs publiques valides pour + + label_webdav: WebDAV + label_full_text: Recherche de texte intégral + link_extension: Ext + + label_webdav_ignore: Modèles de fichiers ignorés + note_webdav_ignore: Expression régulière avec les noms de fichiers qui doivent être ignorés par les requêtes PUT. + + label_document_url: Url + label_last_revision_id: Révision + + label_webdav_disable_versioning: Modèles de fichiers sans gestion de version + note_webdav_disable_versioning: "Expression régulière qui désactive la gestion de version pour les fichiers + correspondants. L'expression par défaut correspond aux fichiers temporaires créés par MsOffice." + + label_dmsf_keep_documents_locked: Garder les documents verrouillés + note_dmsf_keep_documents_locked: Les documents seront maintenus verrouillés après approbation. + note_global: (global) + field_dmsf_not_inheritable: Ne peut être hérité + + label_webdav_use_project_names: Utiliser le nom de projet pour le dossier du projet + note_webdav_use_project_names: "Utilise le nom du projet au lieu de l'identifiant projet pour le dossier du projet." + + label_last_approver: Dernier approbateur + + label_act_as_attachable: Utiliser comme pièce jointe + note_dmsf_act_as_attachable: "Permet de joindre des documents aux objets tels que les demandes." + + label_user_search_add: Rechercher des utilisateurs à ajouter + + label_dmsf_attachments: Pièces-jointes DMS + label_basic_attachments: Pièces-jointes standards + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: Flux + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: Taille + field_locked: Verrouillé + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: Titre + label_column_size: Taille + label_column_modified: Modifié + label_column_version: Version + label_column_workflow: Flux + label_column_author: Auteur + label_column_description: Description + label_column_comment: Commentaire + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: contient un(des) caractère(s) invalide(s) \ No newline at end of file diff --git a/config/locales/hu.yml b/config/locales/hu.yml new file mode 100644 index 00000000..29a3edda --- /dev/null +++ b/config/locales/hu.yml @@ -0,0 +1,497 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +hu: + dmsf: DMS # Custom fields tab title + label_dmsf_file: DMS Dokumtum + label_dmsf_file_plural: DMS Dokumentumok # Email subject & Search options + label_dmsf_file_revision_plural: Dokumtum felülvizsgálata + label_dmsf_file_revision_access_plural: Dokumentum hozzáférései + warning_no_entries_selected: Verzió törölve + error_email_to_must_be_entered: Email fogadóját be kell írni + warning_file_already_locked: Fájlt már lezárták + notice_file_locked: Fájl lezárva + warning_file_not_locked: Fájl nincs lezárva + notice_file_unlocked: Fájl kinyitva + error_only_user_that_locked_file_can_unlock_it: Csak az tudja kinyitni a fájlt aki bezárta + + error_max_files_exceeded: "Túllépte az egyszerre letölthető fájlok %{number} számának korlátozását" + error_entry_project_does_not_match_current_project: Az állomány projektje nem egyezik a jelenlegi projekttel + + notice_folder_created: Mappa létrehozása sikerült + error_folder_creation_failed: Mappa létrehozása nem sikerült + error_folder_title_must_be_entered: Cím beírása kötelező + notice_folder_deleted: Mappa törlésre került + error_folder_title_is_already_used: A címet már használják + notice_folder_details_were_saved: Mappa részleteit mentésre kerültek + error_folder_is_locked: Mappa zárolva + error_file_is_locked: Fájl zárolva + notice_file_deleted: Törölt fájl + error_at_least_one_revision_must_be_present: Legalább egy verziónak szükséges lennie + notice_revision_deleted: Verzió törölve + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: Az egyik fájl zárolva van + notice_file_revision_created: Fájl verzió létrehozva + notice_your_preferences_were_saved: A preferenciái mentése kerültek + notice_your_preferences_were_not_saved: Preferenciái nem lettek mentve + warning_folder_notifications_already_activated: Mappa értesítések már be vannak kapcsolva + + notice_folder_notifications_activated: Mappa értesítések bekapcsolva + warning_folder_notifications_already_deactivated: Mappa értesítések már ki vannak kapcsolva + + notice_folder_notifications_deactivated: Mappa értesítések ki vannak kapcsolva + warning_file_notifications_already_activated: File értesítések már bekapcsolva + + notice_file_notifications_activated: Fájl értesítések aktiválva + warning_file_notifications_already_deactivated: Fájl értesítések már bekapcsolva + + notice_file_notifications_deactivated: Fájl értesítések kikapcsolva + link_details: "%{title} részletek" + link_edit: "%{title} szerkesztése" + link_create_folder: Mappa létrehozása + link_title: Cím + link_size: Méret + link_modified: Módosított + link_ver: Verzió + link_author: Szerző + title_check_for_zip_download_or_email: Kijelölés tömörítésre, letöltésre vagy email-ben küldésre + title_check_for_restore_or_delete: Kijelölés visszaállításra vagy törlésre tömörítésre, letöltésre vagy email-ben + küldésre + title_notifications_active_deactivate: "Értesítések bekapcsolva: Kikapcsolás" + title_notifications_not_active_activate: "Értesítések nincsnek bekapcsolva: Bekapcsolás" + title_title_version_version_download: "%{title} verzió %{version} letöltés" + title_locked_by_user: "Zárolva %{user}" + title_waiting_for_approval: Elfogadásra vár + title_approved: Elfogadva + title_unlock_file: Zárolás megszüntetése, hogy más felhasználók is változtatni tudjanak + title_lock_file: Zárolás, hogy más felhasználók nem tudjank változtatni rajt + title_download_checked: Ellenőrzött letöltése a Zip archívumba + title_send_checked_by_email: Kijelöltek küldése email-ben + link_user_preferences: DMS projekt preferenciák + heading_send_documents_by_email: Dokumentumok küldése emailen + label_email_from: Valakitől + label_email_to: Valakinek + label_email_cc: CC + label_email_subject: Tárgy + label_email_documents: Dokumentumok + label_email_body: Szövegrész + label_email_send: Küldés + title_notifications_active: Értesítések bekapcsolva + label_upload: Feltöltés + heading_new_folder: Új mappa + label_title: Cím + label_description: Leírás + submit_save: Mentés + info_file_locked: Fájl zárolva! + label_notifications: Értesítések + select_option_default: Alapértelmezett + select_option_deactivated: Deaktivált + select_option_activated: Aktivált + label_title_format: Cím formátum + text_title_format: "Dokumentum címformátum a letöltéshez (%t - cím, %f - fájl, %d - dátum, %v - verzió, %i - ID, %r - + revízió) Például: t_%v" + title_save_preferences: Preferenciák mentése + heading_revisions: Verziók + title_download: Letöltés + title_delete_revision: Verzió törlése + title_obsolete_revision: Obsolete revision + label_created: Létrehozott + label_changed: Változtatott + info_changed_by_user: "%{changed} általa" + label_filename: Fájlnév + label_mime: Mime + label_size: Méret + heading_new_revision: Új változat + option_version_same: Ugyanaz + option_version_patch: Patch + option_version_minor: Kisebb + option_version_major: Fő + option_version_custom: Egyedi + label_new_content: Új tartalom + label_maximum_files_download: Maximálisan feltölthető fájlok + note_maximum_number_of_files_downloaded: Maximálisan letölthető zip fájlok, vagy emailek. 0 korlátlan letöltést + jelent. + label_file_storage_directory: Fájl tárolókönyvtár + label_index_database: Index database + label_stemming_language: Törzs nyelv + note_possible_values: Lehetséges értékek + note_pass_none_to_disable_stemming: hagyja 'none' beállításon a csonkolás kikapcsolásához + label_stem_strategy: Csonkolási stratégia + option_stem_none: Nincs csonkolás (alapértelmezett) + option_stem_some: Részleges csonkolás + option_stem_all: Teljes csonkolás + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: Fájl alapértelmezett értesítések + heading_uploaded_files: Feltöltött fájlok + link_documents: Dokumentumok + permission_view_dmsf_file_revision_accesses: Letöltések megjelenítése a tevékenységi listában + permission_view_dmsf_file_revisions: Verziók megjelenítése a tevékenységi listában + permission_view_dmsf_folders: Dokumentumok böngészése + permission_user_preferences: Preferenciák használata + permission_view_dmsf_files: Dokumentumok megtekintése + permission_folder_manipulation: Mappa manipulálás + permission_file_manipulation: Fájl manipulálás + permission_force_file_unlock: Fájl kényszerített feloldása + permission_manage_workflows: Munkamenet kezelése + permission_file_delete: Dokumentumok törlése + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: Fájl + field_folder: Mappa + error_file_commit_require_uploaded_file: Fájl beíratáshoz szükséges a feltöltött fájl + + warning_some_files_were_not_committed: "Néhány fájl nem került beíratásra validálási locked_documents: Zárolt + dokumentum" + error_user_has_not_right_delete_folder: A felhasználónak nincs jogosultsága a mappa törléséhez + + error_user_has_not_right_delete_file: Felhasználónak nincs jogosultsága a fájl törléséhez. + + notice_entries_deleted: Állományok törölve + warning_some_entries_were_not_deleted: "Néhány állomány nem került törlésre %{entries}" + title_delete_checked: Ellenőrzöttek törlése + title_items: Elemek + title_filename_for_download: Letöltéshez vagy .zip tömörített állományhoz használt fájlnév + label_number_of_folders: Mappák + label_number_of_documents: Dokumentumok + error_file_storage_directory_does_not_exist: A fájl tárolómappa nem létezik és nem hozható létre + + error_file_can_not_be_created: A fájl nem hozható létre a tárolómappában + error_wrong_zip_encoding: Nem megfelelő Zip kódolás + warning_xapian_not_available: Xapian nem elérhető + menu_dmsf: DMS # Project tab title + label_physical_file_delete: Fizikai fájl törölve lett + user_is_not_project_member: Ön nem tagja a csoportnak + heading_access_downloads_emails: Letöltések/Emailek + heading_access_first: Első + heading_access_last: Utolsó + label_dmsf_updated: Frissített + label_dmsf_downloaded: Letöltött + title_total_size_of_all_files: Ebbe a mappába tartozó összes fájl mérete + project_module_dmsf: DMS # Project module name + warning_no_project_to_copy_file_to: Nincs projekt ahová másolható a fájl + comment_copied_from: "Másolva %{source}" + field_target_project: Célprojekt + field_target_folder: Cél mappa + title_copy_or_move: Másolás/Elmozgatás + label_dmsf_folder_plural: DMS Mappák # Search options + comment_moved_from: "Elmozgatva %{source}" + error_target_folder_same: A cél mappa és a projekt ugyanaz, mint a jelenlegi + title_copy: Másolás + + error_max_email_filesize_exceeded: "Túllépte a maximum fájlméretet ami emailen küldhető. (%{number} MB)" + + note_maximum_email_filesize: Korlátozza a maximális fájlméretet, amely küldhető email-ben. A 0 korlátlan méretet + jelent, az érték MB-ban kerül megadásra. + label_maximum_email_filesize: Emailhez csatolható fájl maximum mérete. + header_minimum_filesize: Fájl hiba. + error_minimum_filesize: "A fájl %{file} 0 bájt, ezért nem lesz csatolva." + parent_directory: Szülő könyvtár + note_webdav: "Webdav aktiválása után itt található %{protocol}://%{domain}/dmsf/webdav/[projekt azonosító]" + + label_copy_dmsf: "Dokumentumok és mappák (%{files} másolása a %{folders} mappákba)" + label_copy_only_dmsf_folders: "Dokumentumok és mappák (%{folders})" + + warning_folder_already_locked: Ez a mappa már zárolva + notice_folder_locked: A mappa sikeresen lett zárolva + warning_folder_not_locked: Sajnos, a mappát nem lehet zárolni + notice_folder_unlocked: A mappa sikeresen feloldva + error_only_user_that_locked_folder_can_unlock_it: Önnek nincs jogosultsága a mappa megnyitásához + + title_unlock_folder: Feloldás, hogy mások is szerkeszteni tudjanak + title_lock_folder: Mappa zárolása, hogy más felhasználók ne tudjanak változtatást végrehajtani + + select_option_webdav_readonly: Csak-olvasásra + select_option_webdav_readwrite: Olvas/Ír + label_webdav_strategy: Webdav stratégia + + note_webdav_strategy: Engedélyezi az adminisztrátornak, hogy dönthessen a felhasználók + + + error_unable_delete_dmsf_workflow: Workflow törlése nem lehetséges + error_empty_note: A feljegyzés nem lehet üres + error_workflow_assign: Hiba lépet fel a kiosztás során + error_cannot_start_workflow: Workflow nem kezdhető el + error_cannot_renumber_steps: Lépéseket nem lehet úraszámozni + label_dmsf_workflow_new: Új jóváhagyási workflow + field_label_dmsf_workflow: Elfogadott Workflow + field_label_dmsf_workflow_name: Jóváhagyási workflow neve + label_dmsf_workflow_plural: Jóváhagyási folyamatok + label_dmsf_workflow_plural_num: "Jóváhagyási workflow-k (%{count})" + label_dmsf_workflow_step: Lépés + label_dmsf_workflow_step_plural: Lépések + label_dmsf_workflow_approval_plural: Jóváhagyások + label_dmsf_wokflow_action_approve: Jóváhagyás + label_dmsf_wokflow_action_assign: Jóváhagyási workflow hozzárendelése + label_dmsf_wokflow_action_delegate: Kioszt + label_dmsf_wokflow_action_reject: Visszautasít + label_dmsf_wokflow_action_start: Workflow kezdés + label_dmsf_workflow_add_approver: "Új jóváhagyó hozzáadása logikai funkcióval:" + label_or: vagy + label_action: Akció + label_note: Feljegyzés + title_none: Egyik sem + title_rejection: Visszautasítás + title_delegation: Delegálás + title_assignment: Kiosztás + title_start: Kezdés + title_dmsf_workflow_log: Jóváhagyási workflow napló + title_assigned: Kiosztott + title_approval: Jóváhagyás + title_rejected: Visszautasított + title_obsolete: Obsolete + dmsf_and: ÉS + dmsf_or: VAGY + dmsf_new_step: Új lépés + dmsf_new_step_or_approver: Új lépés vagy új jóváhagyó + message_dmsf_wokflow_note: Saját jegyzet + info_revision: "r %{rev}" + link_workflow: Workflow + notice_workflow_started: Jóváhagyási workflow sikeresen elkezdődött + text_email_subject_approved: elfogadott + text_email_subject_rejected: visszautasított + text_email_subject_delegated: delegált + text_email_subject_requires_approval: Ön jóváhagyása szükséges + text_email_subject_updated: frissített + text_email_subject_started: elkezdett + text_email_finished_approved: "A '%{name}' jóváhagyási workflow amely a '%{filename}' dokumentumhoz került + hozzárendelésre, befejeződött és a dokumentum jóváhagyásra került." + text_email_finished_rejected: "A '%{name}' jóváhagyási workflow amely a '%{filename}' dokumentumhoz került + hozzárendelésre, befejeződött és visszautasításra került a következő üzenettel: '%{notice}'." + text_email_finished_delegated: "A '%{name}' jóváhagyási workflow amely a '%{filename}' dokumentumhoz került + hozzárendelésre, Önhöz került az alábbi megjegyzés '%{notice}' miatt és a jelenlegi lépésben az Ön jóváhagyására + vár '%{stepname}'." + text_email_finished_step: "A '%{name}' jóváhagyási workflow-ban amely a '%{filename}' dokumentumhoz került + hozzárendelésre, egy lépés teljesült és a következő lépésben lépésben az Ön jóváhagyására vár." + text_email_finished_step_short: "A '%{name}' jóváhagyási workflow-ban amely a '%{filename}' dokumentumhoz került + hozzárendelésre, egy lépés teljesült a folyamatban." + text_email_started: "A '%{name}' jóváhagyási workflow amely a '%{filename}' dokumentumhoz került hozzárendelésre, + elindult és a jelenlegi lépésben az Ön jóváhagyására vár '%{stepname}'." + text_email_to_proceed: A folytatáshoz kattintson a négyzetre a dokumentum mellett + text_email_to_see_history: Ha meg akarja nézni a jóváhagyási előzményeket, akkor kattintson a dokumentumon belül a + workflow státuszra + text_email_to_see_status: Ha meg szeretné nézni a jóváhagyási folyamatot kattintson a dokumentumon belül a workflow + státuszra + + title_create_link: Szimbolikus link létrehozása (symlink) + label_link_from: Hivatkozás innen + label_link_to: Hivatkozás ide + label_notifications_on: Értesítések bekapcsolva + label_notifications_off: Értesítések kikapcsolva + field_target_file: Forrás file + title_download_entries: Bejegyzések letöltése + label_external: Külső + label_internal: Internal + + label_link_name: Link elnevezése + field_external_url: URL + label_target_folder: Cél mappa + label_source_folder: Forrás mappa + label_target_project: Cél projekt + label_source_project: Forrás projekt + + text_email_doc_updated_subject: Frissített dokumentumok + text_email_doc_updated: éppen most frissültek a dokumentumok + text_email_doc_follows: következőként + text_email_doc_deleted_subject: Törölt dokumentumok + text_email_doc_deleted: éppen most lettek törölve a dokumentumok + label_links_only: csak linkek + + label_display_notified_recipients: Értesített címzettek megjelenítése + note_display_notified_recipients: A felhasználó tájékoztatást kap a címzettekről, + Warning_email_notifications: "Email értesítés lett küldve %{to}" + + link_trash_bin: Lomtár + title_restore: Helyreállít + notice_dmsf_file_restored: A dokumentum sikeresen vissza lett állítva + notice_dmsf_folder_restored: A mappa sikeresen vissza lett állítva + notice_dmsf_link_restored: Link visszaállítása sikeres + title_restore_checked: Visszaállítás ellenőrizve + error_parent_folder: A szülő mappa nem létezik + + error_resource_or_parent_locked: Zárolás sikertelen - erőforrás(vagy szülő) zárolva + error_parent_locked: Zárolás sikertelen - erőforrás szülő zárolva + error_resource_locked: Zárolás sikertelen - erőforrás zárolva + error_lock_exclusively: Nem lehetséges kivételként zárolni egy már zárolt erőforrást + error_unlock_parent_locked: Feloldás sikertelen - szülő erőforrás zárolva + + label_dmsf_version: Verzió + + locked_documents: Locked documents + open_approvals: Nyitott jóváhagyások + watched_documents: Watched documents + + error_maximum_upload_filecount: "Nem több, mint %{filecount} fájl tölthető fel." + + label_public_urls: Public URLs valid to + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Ignored files patterns + note_webdav_ignore: A regular expresion with filenames to ignore by PUT requests. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. + + label_dmsf_keep_documents_locked: Keep documents locked + note_dmsf_keep_documents_locked: Documents will be kept locked when approved + note_global: (global) + field_dmsf_not_inheritable: Not inheritable + + label_webdav_use_project_names: Use project name for project folder + note_webdav_use_project_names: Use project names instead of project identifier for project folders. + + label_last_approver: Last approver + + label_act_as_attachable: Act as attachable + note_dmsf_act_as_attachable: Allows to attach documents to objects e.g. issues. + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: Workflow + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: Méret + field_locked: Lezárták + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: Cím + label_column_size: Méret + label_column_modified: Módosítva + label_column_version: Verzió + label_column_workflow: Workflow + label_column_author: Szerző + label_column_description: Leírás + label_column_comment: Megjegyzés + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: érvénytelen karaktereket tartalmaz \ No newline at end of file diff --git a/config/locales/it.yml b/config/locales/it.yml new file mode 100644 index 00000000..43cc1f6c --- /dev/null +++ b/config/locales/it.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +it: # Italian strings thx 2 Matteo Arceci! + dmsf: DMS # Custom fields tab title + label_dmsf_file: DMS Documento + label_dmsf_file_plural: DMS Documenti # Email subject & Search options + label_dmsf_file_revision_plural: Revisioni al documento + label_dmsf_file_revision_access_plural: Accessi al documento + warning_no_entries_selected: Nessun documento selezionato + error_email_to_must_be_entered: Deve essere inserito l'indirizzo email + warning_file_already_locked: Documento già bloccato + notice_file_locked: Documento bloccato + warning_file_not_locked: Documento non bloccato + notice_file_unlocked: Documento sbloccato + error_only_user_that_locked_file_can_unlock_it: Solo chi ha bloccato il documento può sbloccarlo + + error_max_files_exceeded: "Superato il limite di %{number} documenti per il download simultaneo" + error_entry_project_does_not_match_current_project: "Il progetto non corrisponde al progetto corrente" + + notice_folder_created: Cartella creata + error_folder_creation_failed: Creazione cartella fallita + error_folder_title_must_be_entered: Deve essere inserito il titolo + notice_folder_deleted: Cartella cancellata + error_folder_title_is_already_used: Il titolo è già in uso + notice_folder_details_were_saved: I dettagli della cartella sono stati salvati + error_folder_is_locked: La cartella è bloccata + error_file_is_locked: Il documento è bloccato + notice_file_deleted: Il documento è stato cancellato + error_at_least_one_revision_must_be_present: Deve essere presente almeno una revisione + notice_revision_deleted: Revisione cancellata + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: Uno dei documenti è bloccato + notice_file_revision_created: Revisione del documento creata + notice_your_preferences_were_saved: Le tue preferenze sono state salvate + notice_your_preferences_were_not_saved: Le tue preferenze non sono state salvate + warning_folder_notifications_already_activated: La notifica su cartella è già attiva + + notice_folder_notifications_activated: La notifica su cartella è stata attivata + warning_folder_notifications_already_deactivated: La notifica su cartella è già disattiva + + notice_folder_notifications_deactivated: La notifica su cartella è stat disattivata + warning_file_notifications_already_activated: La notifica su documento è già attiva + + notice_file_notifications_activated: La notifica su documento è stata attivata + warning_file_notifications_already_deactivated: La notifica su documento è già disattiva + + notice_file_notifications_deactivated: La notifica su documento è disattiva + link_details: "%{title} dettagli" + link_edit: "Modifica %{title}" + link_create_folder: Crea cartella + link_title: Nome documento + link_size: Dimensioni + link_modified: Modificato + link_ver: Ver. + link_author: Autore + title_check_for_zip_download_or_email: Seleziona per zip, download o email + title_check_for_restore_or_delete: Seleziona per ripristinare o cancellare + + title_notifications_active_deactivate: "Notifiche attive: Disattiva" + title_notifications_not_active_activate: "Notifiche disattivate: Attiva" + title_title_version_version_download: "%{title} versione %{version} download" + title_locked_by_user: "Bloccato da %{user}" + title_waiting_for_approval: In attesa di Approvazione + title_approved: Approvato + title_unlock_file: Sblocca per consentire modifiche degli altri membri + title_lock_file: Blocca per evitare modifiche degli altri membri + title_download_checked: Download selezionati in archivio Zip + title_send_checked_by_email: Selezionati spediti per email + link_user_preferences: Le tue preferenze DMS di progetto + heading_send_documents_by_email: Documenti spediti per email + label_email_from: Da + label_email_to: A + label_email_cc: CC + label_email_subject: Oggetto + label_email_documents: Documenti + label_email_body: Corpo + label_email_send: Spedisci + title_notifications_active: Notifiche attive + label_upload: Carica + heading_new_folder: Nuova cartella + label_title: Titolo + label_description: Descrizione + submit_save: Salva + info_file_locked: Documento bloccato! + label_notifications: Notifiche + select_option_default: Default + select_option_deactivated: Disattivato + select_option_activated: Attivato + label_title_format: Titolo formattato + text_title_format: "Titolo del documento formattato per il download (%t - titolo, %d - data, %v - versione, %i - ID, + %r - revisione). Esempio: %t_%v" + title_save_preferences: Salva preferenze + heading_revisions: Revisioni + title_download: Download + title_delete_revision: Cancella revisione + title_obsolete_revision: Obsolete revision + label_created: Creato + label_changed: Modificato + info_changed_by_user: "%{changed} da" + label_filename: Nome file + label_mime: Mime + label_size: Dimensioni + heading_new_revision: Nuova revisione + option_version_same: Stessa + option_version_patch: Patch + option_version_minor: Miniore + option_version_major: Maggiore + option_version_custom: Personalizzata + label_new_content: Nuovo contenuto + label_maximum_files_download: Numero massimo di documenti scaricabili + note_maximum_number_of_files_downloaded: Limita il numero massimo di documenti scaricabili in archivio zip o spediti + via email. 0 significa senza limiti. + label_file_storage_directory: Cartella dei documenti + label_index_database: Indice database + label_stemming_language: Linguaggio di Stemming + note_possible_values: Valori possibili + note_pass_none_to_disable_stemming: "passa 'nulla' per disabilitare lo stemming" + label_stem_strategy: Strategia dello Stem + option_stem_none: Stem nulla (default) + option_stem_some: Stem qualcosa + option_stem_all: Stem tutto + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: Notifica di default dei documenti + heading_uploaded_files: Documenti caricati + link_documents: Documenti + permission_view_dmsf_file_revision_accesses: Visualizza i download nel flusso di attività + permission_view_dmsf_file_revisions: Visualizza le revisioni nel flusso di attività + permission_view_dmsf_folders: Sfoglia i documenti + permission_user_preferences: Preferenze + permission_view_dmsf_files: Visualizza i documenti + permission_folder_manipulation: Modifica la cartella + permission_file_manipulation: Modifica il documento + permission_force_file_unlock: Forza lo sblocco del documento + permission_manage_workflows: Gestisci i flussi di lavoro + permission_file_delete: Elimina i documenti + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: Documento + field_folder: Cartella + error_file_commit_require_uploaded_file: L'aggiornamento del documento richiede un documento già caricato in + precedenza + warning_some_files_were_not_committed: "Alcuni documenti non sono stati aggiornati a causa di errori di validazione: + %{files}" + error_user_has_not_right_delete_folder: "L'utente non ha i diritti per eliminare le cartelle" + + error_user_has_not_right_delete_file: "L'utente non ha i diritti per eliminare il documento" + + notice_entries_deleted: Voci eliminate + warning_some_entries_were_not_deleted: "Alcune voci non sono state eliminate: %{entries}" + title_delete_checked: Elimina i selezionati + title_items: elementi + title_filename_for_download: Nome file utilizzato per il download o nell'archivio Zip + label_number_of_folders: Cartelle + label_number_of_documents: Documenti + error_file_storage_directory_does_not_exist: "La cartella di archiviazione non esiste e non può essere creata" + + error_file_can_not_be_created: "Il documento non può essere creato nella cartella di archiviazione" + error_wrong_zip_encoding: Errato encoding dello Zip + warning_xapian_not_available: Xapian non disponibile + menu_dmsf: Documenti # Project tab title + label_physical_file_delete: File fisico eliminato + user_is_not_project_member: Non sei un membro del progetto + heading_access_downloads_emails: Downloads/Emails + heading_access_first: Primo + heading_access_last: Ultimo + label_dmsf_updated: Aggiornato + label_dmsf_downloaded: Scaricato + title_total_size_of_all_files: Dimensione totale di tutti i files in questa cartella + project_module_dmsf: DMS # Project module name + warning_no_project_to_copy_file_to: Nessun progetto nel quale copiare il documento + comment_copied_from: "Copiato da %{source}" + field_target_project: Progetto di destinazione + field_target_folder: Cartella di destinazione + title_copy_or_move: Copia/Sposta + label_dmsf_folder_plural: DMS Cartelle # Search options + comment_moved_from: "Spostato da %{source}" + error_target_folder_same: La cartella di destinazione ed il progetto sono gli stessi di adesso + title_copy: Copia + + error_max_email_filesize_exceeded: "Hai superato la dimensione massima del file per l'invio tramite e-mail. + (%{number} MB)" + note_maximum_email_filesize: Limiti di dimensione massima dei file che possono essere inviati via e-mail. 0 significa + illimitato. Il numero è in MB. + label_maximum_email_filesize: Dimensione massima degli allegati delle e-mail + header_minimum_filesize: Errore file. + error_minimum_filesize: "Il file %{file} è 0 bytes e non sarà allegato." + parent_directory: Cartella superiore + note_webdav: "Una volta abilitato il Webdav può essere contattato sul percorso + %{protocol}://%{domain}/dmsf/webdav/[project identifier]" + label_copy_dmsf: "Copia documenti e cartelle (%{files} documenti in %{folders} cartelle)" + label_copy_only_dmsf_folders: "Copia solo documenti (%{folders})" + + warning_folder_already_locked: Questa cartella è già bloccata + notice_folder_locked: La cartella è stata bloccata + warning_folder_not_locked: Purtroppo la cartella non può essere bloccata + notice_folder_unlocked: La cartella è stata sbloccata + error_only_user_that_locked_folder_can_unlock_it: Non sei autorizzato a sbloccare questa cartella + + title_unlock_folder: Sblocca per consentire modifiche agli altri membri + title_lock_folder: Blocca per evitare modifiche da parte di altri membri + + select_option_webdav_readonly: Sola lettura + select_option_webdav_readwrite: Lettura/Scrittura + label_webdav_strategy: Strategia Webdav + + note_webdav_strategy: Abilita l'amministratore a decidere se Webdav è di sola lettura oppure lettura-scrittura per gli + utenti finali. + + error_unable_delete_dmsf_workflow: Impossibile eliminare il flusso di lavoro + error_empty_note: "La nota non può essere vuota" + error_workflow_assign: C'è stato un errore durante l'assegnazione + error_cannot_start_workflow: "Il flusso di lavoro non può partire" + error_cannot_renumber_steps: "I passi non possono essere numerati" + label_dmsf_workflow_new: Nuova 'approvazione di flusso di lavoro' + field_label_dmsf_workflow: Approvazione di flusso di lavoro + field_label_dmsf_workflow_name: Nome per l'approvazione di flusso di lavoro + label_dmsf_workflow_plural: Approvazioni flusso di lavoro + label_dmsf_workflow_plural_num: Approvazioni flusso di lavoro (%{count}) + label_dmsf_workflow_step: Passo + label_dmsf_workflow_step_plural: Passi + label_dmsf_workflow_approval_plural: Approvazioni + label_dmsf_wokflow_action_approve: Approva + label_dmsf_wokflow_action_reject: Rifiuta + label_dmsf_wokflow_action_delegate: Delega a + label_dmsf_wokflow_action_assign: Assegna un 'approvazione di flusso di lavoro' + label_dmsf_wokflow_action_start: Inizia un flusso di lavoro + label_dmsf_workflow_add_approver: "Aggiungi un nuovo approvatore con funzioni logiche:" + label_or: oppure + label_action: Azione + label_note: Note + title_none: Nessuno + title_rejection: Rifiuto + title_delegation: Delega + title_assignment: Assegnazione + title_start: Inizio + title_dmsf_workflow_log: Log di approvazione di flusso di lavoro + title_assigned: Assegnato + title_approval: Approvazione + title_rejected: Rifiutato + title_obsolete: Obsolete + dmsf_and: AND + dmsf_or: OR + dmsf_new_step: Nuovo passo + dmsf_new_step_or_approver: Nuovo passo oppure Nuovo approver + message_dmsf_wokflow_note: La tua nota... + info_revision: "r %{rev}" + link_workflow: Flusso di lavoro + notice_workflow_started: Approvazione di flusso di lavoro inizializzato + text_email_subject_approved: "Approvazione di flusso di lavoro %{name} approvato" + text_email_subject_rejected: "Approvazione di flusso di lavoro %{name} rifiutato" + text_email_subject_delegated: "Approvazione di flusso di lavoro %{name} delegato" + text_email_subject_requires_approval: "Approvazione di flusso di lavoro %{name} richiede la tua approvazione" + text_email_subject_updated: "Approvazione di flusso di lavoro %{name} aggiornato" + text_email_subject_started: "Approvazione di flusso di lavoro %{name} inizializzato" + text_email_finished_approved: "L'approvazione di flusso di lavoro '%{name}' assegnato a '%{filename}' è stata + completata ed il documento è stato approvato." + text_email_finished_rejected: "L'approvazione di flusso di lavoro '%{name}' assegnato a '%{filename}' è stata + completata ma il documento è stato rifiutato a causa di '%{notice}'." + text_email_finished_delegated: "L'approvazione di flusso di lavoro '%{name}' assegnato a '%{filename}' è stata + delegata a causa di '%{notice}' e sei tenuto ad approvare nel passo corrente '%{stepname}'." + text_email_finished_step: "L'approvazione di flusso di lavoro '%{name}' assegnato a '%{filename}' ha completato uno + dei passi di approvazione e sei tenuto ad approvare il prossimo passo." + text_email_finished_step_short: "L'approvazione di flusso di lavoro '%{name}' assegnato a '%{filename}' ha completato + uno dei passi di approvazione." + text_email_started: "L'approvazione di flusso di lavoro '%{name}' assegnato a '%{filename}' è stato inizializzato e + sei tenuto ad approvare il passo corrente '%{stepname}'." + text_email_to_proceed: Per proseguire clicca sull'icona checkbox a fianco del documento + text_email_to_see_history: Per vedere la storia di approvazione clicca sullo stato del flusso di lavoro del documento + + text_email_to_see_status: Per vedere lo stato attuale del flusso di lavoro di approvazione clicca sullo stato del + flusso di lavoro del documento + + title_create_link: Crea un collegamento + label_link_from: Collegamento da + label_link_to: Collegamento a + label_notifications_on: Attiva notifiche + label_notifications_off: Disattiva notifiche + field_target_file: Scarica sorgente + title_download_entries: Scarica documenti + label_external: Esterno + label_internal: Internal + + label_link_name: Nome del collegamento + field_external_url: URL + label_target_folder: Cartella di destinazione + label_source_folder: Cartella sorgente + label_target_project: Progetto di destinazione + label_source_project: Progetto sorgente + + text_email_doc_updated_subject: "Documenti del progetto %{project} caricati" + text_email_doc_updated: ha documenti appena attualizzati + text_email_doc_follows: come segue + text_email_doc_deleted_subject: "Documenti del progetto %{project} cancellati" + text_email_doc_deleted: ha documenti appena eliminati + label_links_only: solo colegamenti + + label_display_notified_recipients: Visualizza i destinatari notificati + note_display_notified_recipients: L'utente sarà informato di tutti i destinatari appena inviato la notifica e-mail. + + warning_email_notifications: "Notifica email inviata a %{to}" + + link_trash_bin: Cestino + title_restore: Ripristina + notice_dmsf_file_restored: Il documento è stato ripristinato correttamente + notice_dmsf_folder_restored: La cartella è stata ripristinata correttamente + notice_dmsf_link_restored: Il collegamento è stato ripristinato correttamente + title_restore_checked: Ripristina selezionati + error_parent_folder: "La cartella padre non esiste" + + error_resource_or_parent_locked: Impossibile completare il blocco - la risorsa (o superiore) è bloccata + error_parent_locked: Impossibile completare il blocco - la risorsa superiore è bloccata + error_resource_locked: Impossibile completare il blocco - la risorsa è bloccata + error_lock_exclusively: impossibile bloccare in modo esclusivo una risorsa già bloccata + error_unlock_parent_locked: Sblocco fallito - la risorsa superiore è bloccata + + label_dmsf_version: Versione + + locked_documents: Documenti bloccati + open_approvals: Approvazioni aperte + watched_documents: Watched documents + + error_maximum_upload_filecount: "No more than %{filecount} file(s) can be uploaded." + + label_public_urls: Public URLs valid to + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Ignored files patterns + note_webdav_ignore: A regular expresion with filenames to ignore by PUT requests. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. The default pattern + matches temporary files created by MsOffice. + + label_dmsf_keep_documents_locked: Keep documents locked + note_dmsf_keep_documents_locked: Documents will be kept locked when approved + note_global: (global) + field_dmsf_not_inheritable: Not inheritable + + label_webdav_use_project_names: Use project name for project folder + note_webdav_use_project_names: Use project names instead of project identifier for project folders. + + label_last_approver: Last approver + + label_act_as_attachable: Considera allegabili + note_dmsf_act_as_attachable: Consente di allegare documenti agli oggetti (es. segnalazioni) + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Modifica contenuto + field_workflow: Workflow + field_modified: Data modifica + field_updated: Data aggiornamento + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: Dimensione + field_locked: Bloccato + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copia (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: Colonne DMS + label_column_id: ID + label_column_title: Titolo + label_column_size: Dimensione + label_column_modified: Modificato + label_column_version: Versione + label_column_workflow: Workflow + label_column_author: Autore + label_column_description: Descrizione + label_column_comment: Commenti + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: contiene carattere(i) non validi diff --git a/config/locales/ja.yml b/config/locales/ja.yml new file mode 100644 index 00000000..79a2ff00 --- /dev/null +++ b/config/locales/ja.yml @@ -0,0 +1,499 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +ja: + dmsf: DMS # Custom fields tab title + label_dmsf_file: DMS Document + label_dmsf_file_plural: DMS 文書管理ファイル # Email subject & Search options + label_dmsf_file_revision_plural: 文書管理ファイルリビジョン + label_dmsf_file_revision_access_plural: 文書管理ファイルアクセス + warning_no_entries_selected: エントリーが選ばれていません + error_email_to_must_be_entered: メールの宛先は省略できません + warning_file_already_locked: ファイルは既にロックされています + notice_file_locked: ファイルをロックしました + warning_file_not_locked: ファイルはロックされていません + notice_file_unlocked: ファイルをロック解除しました + error_only_user_that_locked_file_can_unlock_it: ファイルをロックしたユーザーだけがロック解除できます + + error_max_files_exceeded: "同時にダウンロードできるファイル数の上限 %{number} を超えています" + error_entry_project_does_not_match_current_project: 指定したプロジェクトは現在のプロジェクトと一致しません + + notice_folder_created: フォルダを作成しました + error_folder_creation_failed: フォルダを作成できません + error_folder_title_must_be_entered: タイトルが必要です + notice_folder_deleted: フォルダを削除しました + error_folder_title_is_already_used: タイトルは既に使われています + notice_folder_details_were_saved: フォルダの詳細を保存しました + error_folder_is_locked: フォルダはロックされています + error_file_is_locked: ファイルはロックされています + notice_file_deleted: ファイルを削除しました + error_at_least_one_revision_must_be_present: 少なくとも1つのリビジョンが必要です + notice_revision_deleted: リビジョンを削除しました + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: ファイルのうちの1つがロックされています + notice_file_revision_created: ファイルのリビジョンを作成しました + notice_your_preferences_were_saved: 設定を保存しました + notice_your_preferences_were_not_saved: 設定が保存されていません + warning_folder_notifications_already_activated: フォルダ通知は既に有効です + + notice_folder_notifications_activated: フォルダ通知を有効にしました + warning_folder_notifications_already_deactivated: フォルダ通知は既に無効です + + notice_folder_notifications_deactivated: フォルダ通知を無効にしました + warning_file_notifications_already_activated: ファイル通知は既に有効です + + notice_file_notifications_activated: ファイル通知を有効にしました + warning_file_notifications_already_deactivated: ファイル通知は既に無効です + + notice_file_notifications_deactivated: ファイル通知を無効にしました + link_details: "%{title} の詳細を表示" + link_edit: "%{title} を編集" + link_create_folder: フォルダ作成 + link_title: タイトル + link_size: サイズ + link_modified: 更新日時 + link_ver: バージョン + link_author: 作成者 + title_check_for_zip_download_or_email: Zip圧縮してダウンロードまたはメールするにはチェック + title_check_for_restore_or_delete: 復元または削除するにはチェック + + title_notifications_active_deactivate: "通知は有効です: 無効にする" + title_notifications_not_active_activate: "通知は無効です: 有効にする" + title_title_version_version_download: "%{title} のバージョン %{version} をダウンロードする" + title_locked_by_user: "%{user} によってロックされています" + title_waiting_for_approval: 承認待ち + title_approved: 承認済み + title_unlock_file: ロック解除して他のメンバーの変更を許可する + title_lock_file: ロックして他のメンバーの変更を禁止する + title_download_checked: チェックしたアイテムをZipアーカイブでダウンロードする + title_send_checked_by_email: チェックしたアイテムをメールで送信する + link_user_preferences: 文書管理の設定 + heading_send_documents_by_email: メールで文書を送信する + label_email_from: 差出人 + label_email_to: 宛先 + label_email_cc: CC + label_email_subject: 件名 + label_email_documents: 文書 + label_email_body: 本文 + label_email_send: 送信 + title_notifications_active: 通知は有効です + label_upload: アップロード + heading_new_folder: 新規フォルダ + label_title: タイトル + label_description: 説明 + submit_save: 保存 + info_file_locked: ファイルをロックしました! + label_notifications: 通知 + select_option_default: 既定値 + select_option_deactivated: 無効 + select_option_activated: 有効 + label_title_format: タイトル形式 + text_title_format: "ダウンロード時の文書タイトル形式 (%t - タイトル, %f - ファイル名, %d - 日時, %v - バージョン, %i - ID, %r - + リビジョン)。 例: %t_%v" + title_save_preferences: 設定を保存する + heading_revisions: リビジョン + title_download: ダウンロードする + title_delete_revision: リビジョンを削除する + title_obsolete_revision: Obsolete revision + label_created: 作成者/日時 + label_changed: 更新者/日時 + info_changed_by_user: "/ %{changed}" + label_filename: ファイル名 + label_mime: 種類 + label_size: サイズ + heading_new_revision: 新しいリビジョン + option_version_same: 変更なし + option_version_patch: Patch + option_version_minor: マイナー + option_version_major: メジャー + option_version_custom: カスタム + label_new_content: 新規コンテンツ + label_maximum_files_download: 最大ファイルダウンロード数 + note_maximum_number_of_files_downloaded: Zip でダウンロードできる、またはメールで送信できるファイル数の上限。0は無制限。 + + label_file_storage_directory: ファイル保存フォルダ + label_index_database: インデックスデータベース + label_stemming_language: 語幹抽出する言語 + note_possible_values: 取りうる値 + note_pass_none_to_disable_stemming: 語幹抽出を無効にするには 'none' を設定します。 + label_stem_strategy: 抽出方針 + option_stem_none: 抽出しない (既定値) + option_stem_some: Stem some + option_stem_all: すべて抽出 + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: ファイル通知の既定値 + heading_uploaded_files: アップロードされたファイル + link_documents: 文書 + permission_view_dmsf_file_revision_accesses: ダウンロード履歴の表示 + permission_view_dmsf_file_revisions: リビジョン履歴の表示 + permission_view_dmsf_folders: ファイルの一覧 + permission_user_preferences: ユーザー設定 + permission_view_dmsf_files: ファイルの表示 + permission_folder_manipulation: フォルダの操作 + permission_file_manipulation: ファイルの操作 + permission_force_file_unlock: ファイルの強制ロック解除 + permission_manage_workflows: ワークフローの管理 + permission_file_delete: ファイルの削除 + permission_display_system_folders: システムフォルダを表示 + permission_file_approval: ファイルの承認 + permission_email_documents: 文書をメールで送信 + label_file: ファイル + field_folder: フォルダ + error_file_commit_require_uploaded_file: コミットするには、まずファイルをアップロードしてください + + warning_some_files_were_not_committed: "いくつかのファイルは、バリデーションエラーのためにコミットされませんでした: %{files}" + + error_user_has_not_right_delete_folder: ユーザーにフォルダを削除する権限がありません + + error_user_has_not_right_delete_file: ユーザーにファイルを削除する権限がありません + + notice_entries_deleted: エントリーを削除しました + warning_some_entries_were_not_deleted: "いくつかのエントリーは削除されませんでした: %{entries}" + title_delete_checked: チェックしたアイテムを削除する + title_items: アイテム + title_filename_for_download: ファイル名はダウンロードまたは Zip アーカイブに使われます + label_number_of_folders: フォルダ + label_number_of_documents: 文書 + error_file_storage_directory_does_not_exist: ファイルの保存フォルダが存在せず、作成もできません + + error_file_can_not_be_created: ファイルを保存フォルダに作成できません + error_wrong_zip_encoding: Zip エンコーディングが正しくありません + warning_xapian_not_available: Xapian は利用できません + menu_dmsf: 文書管理 # Project tab title + label_physical_file_delete: 物理ファイルの削除 + user_is_not_project_member: あなたはプロジェクトのメンバーではありません + heading_access_downloads_emails: ダウンロード/メール + heading_access_first: 初回アクセス + heading_access_last: 最終アクセス + label_dmsf_updated: 文書管理 更新 + label_dmsf_downloaded: 文書管理 ダウンロード + title_total_size_of_all_files: このフォルダにある全ファイルの合計サイズ + project_module_dmsf: 文書管理 # Project module name + warning_no_project_to_copy_file_to: ファイルのコピー先プロジェクトが存在しません + comment_copied_from: "%{source} からコピーしました" + field_target_project: ターゲットプロジェクト + field_target_folder: ターゲットフォルダ + title_copy_or_move: コピー/移動 + label_dmsf_folder_plural: DMS フォルダ # Search options + comment_moved_from: "%{source} から移動しました" + error_target_folder_same: コピー/移動先のフォルダとプロジェクトが現在と同じです + title_copy: コピー + + error_max_email_filesize_exceeded: "メールで送信可能なファイルサイズの上限を越えています (%{number} MB)" + + note_maximum_email_filesize: "メールで送信可能なファイルサイズの上限。0は無制限。単位はMB。" + + label_maximum_email_filesize: "メール添付ファイルサイズ上限" + header_minimum_filesize: "ファイルエラー" + error_minimum_filesize: " %{file} のサイズは0バイトのため添付することはできません。" + parent_directory: "親フォルダ" + note_webdav: "WebDAVを有効にすると %{protocol}://%{domain}/dmsf/webdav/[project identifier] で閲覧することができます" + + label_copy_dmsf: "ファイルとフォルダをコピーします (%{files} ファイルを含む %{folders} フォルダ)" + label_copy_only_dmsf_folders: "ファイルとフォルダをコピーします (%{folders})" + + warning_folder_already_locked: "このフォルダは既にロックされています" + notice_folder_locked: "フォルダをロックしました" + warning_folder_not_locked: "フォルダをロックすることができませんでした" + notice_folder_unlocked: "フォルダのロックを解除しました" + error_only_user_that_locked_folder_can_unlock_it: "このフォルダのロックを解除する権限がありません" + + title_unlock_folder: "他のメンバーの変更を許可するためにロック解除する" + title_lock_folder: "他のメンバーによる変更を禁止するためにロックする" + + select_option_webdav_readonly: "読み取り専用" + select_option_webdav_readwrite: "読み書き可能" + label_webdav_strategy: "WebDAVアクセス制御" + + note_webdav_strategy: "WebDAVを使ったアクセスの制御(読み取り専用または読み書き可能)を決定します。" + + + error_unable_delete_dmsf_workflow: そのワークフローの削除はできません + error_empty_note: コメント欄が空です + error_workflow_assign: ワークフローの適用処理中にエラーは発生しました + error_cannot_start_workflow: ワークフローを開始することができませんでした + error_cannot_renumber_steps: ステップの順番の変更ができませんでした + label_dmsf_workflow_new: 新規承認ワークフロー + field_label_dmsf_workflow: 承認ワークフロー + field_label_dmsf_workflow_name: 承認ワークフロー名 + label_dmsf_workflow_plural: 承認ワークフロー + label_dmsf_workflow_plural_num: 承認ワークフロー数 (%{count}) + label_dmsf_workflow_step: ステップ + label_dmsf_workflow_step_plural: ステップ + label_dmsf_workflow_approval_plural: 承認 + label_dmsf_wokflow_action_approve: 承認 + label_dmsf_wokflow_action_reject: 否認 + label_dmsf_wokflow_action_delegate: 代理承認 + label_dmsf_wokflow_action_assign: 承認ワークフロー設定 + label_dmsf_wokflow_action_start: ワークフロー開始 + label_dmsf_workflow_add_approver: "新規承認者の追加:" + label_or: または + label_action: アクション + label_note: コメント + title_none: 無し + title_rejection: 否認 + title_delegation: 代理承認 + title_assignment: アサイン + title_start: 開始 + title_dmsf_workflow_log: 承認ワークフローの履歴 + title_assigned: アサイン + title_approval: 承認 + title_rejected: 否認 + title_obsolete: Obsolete + dmsf_and: AND + dmsf_or: OR + dmsf_new_step: 新規ステップ + dmsf_new_step_or_approver: 新規ステップまたは新規承認者 + message_dmsf_wokflow_note: コメント + info_revision: "r %{rev}" + link_workflow: ワークフロー + notice_workflow_started: 承認ワークフローが開始されました + text_email_subject_approved: は承認されました + text_email_subject_rejected: は否認されました + text_email_subject_delegated: は代理承認が設定されました + text_email_subject_requires_approval: はあなたの承認待ちです + text_email_subject_updated: が更新されました + text_email_subject_started: が開始されました" + text_email_finished_approved: "承認ワークフロー '%{name}' において '%{filename}' が承認されました。" + + text_email_finished_rejected: "承認ワークフロー '%{name}' において '%{filename}' が否認されました。理由:'%{notice}'。" + + text_email_finished_delegated: "承認ワークフロー '%{name}' において代理承認が依頼されました。承認対象 '%{filename}' + の内容をご確認の上、承認・否認のご判断をお願い致します(依頼主コメント:'%{notice}')。'%{stepname}'" + text_email_finished_step: "承認ワークフロー '%{name}' からの承認依頼です。承認対象 '%{filename}' + の内容をご確認の上、承認・否認のご判断をお願い致します。" + text_email_finished_step_short: "承認ワークフロー '%{name}' において '%{filename}' の承認ステップが一つ終了しました。" + + text_email_started: "承認ワークフロー '%{name}' からの承認依頼です。承認対象 '%{filename}' + の内容をご確認の上、承認・否認のご判断をお願い致します。'%{stepname}'" + text_email_to_proceed: 次のURLを開き内容を確認の上、右端のチェックマークをクリックし承認・否認の選択をしてください。 + text_email_to_see_history: 次のURLで承認ワークフローの履歴を確認することができます。 + + text_email_to_see_status: 次のURLで承認ワークフローのステータスを確認することができます。 + + + title_create_link: シンボリックリンクを作成する + label_link_from: リンク元 + label_link_to: リンク先 + label_notifications_on: 通知オン + label_notifications_off: 通知オフ + field_target_file: リンク元ファイル + title_download_entries: ダウンロード履歴 + label_external: 外部 + label_internal: 内部 + + label_link_name: リンク名 + field_external_url: URL + label_target_folder: リンク先フォルダ + label_source_folder: リンク元フォルダ + label_target_project: リンク先プロジェクト + label_source_project: リンク元プロジェクト + + text_email_doc_updated_subject: ファイルが更新されました + text_email_doc_updated: が次のファイルを更新しました。 + text_email_doc_follows: 対象ファイル: + text_email_doc_deleted_subject: ファイルが削除されました + text_email_doc_deleted: が次のファイルを削除しました。 + label_links_only: リンクのみ + + label_display_notified_recipients: 通知受信者の表示 + note_display_notified_recipients: メールで通知したすべてのユーザーの情報を表示します。 + + warning_email_notifications: "%{to} へメールで通知しました" + + link_trash_bin: ゴミ箱 + title_restore: 復元 + notice_dmsf_file_restored: ファイルを復元しました + notice_dmsf_folder_restored: フォルダを復元しました + notice_dmsf_link_restored: リンクを復元しました + title_restore_checked: チェックしたアイテムを復元する + error_parent_folder: 親フォルダが存在しません + + error_resource_or_parent_locked: ロックを完了できません - リソース(または親フォルダ)がロックされています + error_parent_locked: ロックを完了できません - リソースの親フォルダがロックされています + error_resource_locked: ロックを完了できません - リソースがロックされています + error_lock_exclusively: 既にロックされているリソースを排他的にロックできません + error_unlock_parent_locked: ロック解除に失敗しました - リソースの親フォルダがロックされています + + label_dmsf_version: バージョン + + locked_documents: ロック中 + open_approvals: 未承認 + watched_documents: Watched documents + + error_maximum_upload_filecount: 一度にアップロードできるファイルは %{files} つまでです。 + + label_public_urls: 公開用URL + + label_webdav: WebDAV + label_full_text: 全文検索 + link_extension: 拡張子 + + label_webdav_ignore: 無視するファイルパターン + note_webdav_ignore: PUTリクエスト時に無視するファイル名を正規表現で指定します。 + + label_document_url: URL + label_last_revision_id: リビジョン + + label_webdav_disable_versioning: バージョン管理を無視するファイルパターン + note_webdav_disable_versioning: 正規表現にマッチしたファイルのバージョン管理を無効にします。デフォルトのパターンは + MS Officeによって作成される一時ファイルにマッチします。 + + label_dmsf_keep_documents_locked: 文書をロックしたままにする + note_dmsf_keep_documents_locked: 承認されると文書もロックされた状態にする + note_global: (global) + field_dmsf_not_inheritable: 継承しない + + label_webdav_use_project_names: プロジェクトフォルダにプロジェクト名を使用 + note_webdav_use_project_names: プロジェクト識別子の代わりに、プロジェクト名をプロジェクトフォルダとして使用する。 + + label_last_approver: 最終承認者 + + label_act_as_attachable: 貼り付けを許可 + note_dmsf_act_as_attachable: 文書を「チケット」などに貼り付けることを許可します。 + + label_user_search_add: 追加するユーザーを検索 + + label_dmsf_attachments: DMS 添付ファイル + label_basic_attachments: 標準 添付ファイル + + label_email_from_override: 差出人 + text_email_from_override: 現在ログインしているユーザー + label_email_reply_to: 返信先 + + 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" + + 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 + or folders in order to speed up the process of creating links. + + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: ワークフロー + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: サイズ + field_locked: Locked + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: タイトル + label_column_size: サイズ + label_column_modified: 変更 + label_column_version: バージョン + label_column_workflow: ワークフロー + label_column_author: 作成者 + label_column_description: 説明 + label_column_comment: コメント + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: 無効な文字を含んでいます diff --git a/config/locales/ko.yml b/config/locales/ko.yml new file mode 100644 index 00000000..941a64b9 --- /dev/null +++ b/config/locales/ko.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +ko: + dmsf: DMS + label_dmsf_file: DMS Document + label_dmsf_file_plural: DMS 문서 + label_dmsf_file_revision_plural: 문서 리비전 + label_dmsf_file_revision_access_plural: 문서 접근 + warning_no_entries_selected: 선택된 항목이 없습니다 + error_email_to_must_be_entered: 전자 우편 수신자를 입력하셔야 합니다 + warning_file_already_locked: 파일이 이미 잠겨있습니다 + notice_file_locked: 파일 잠금 + warning_file_not_locked: 파일이 잠겨있지 않습니다 + notice_file_unlocked: 파일 잠금 해제 + error_only_user_that_locked_file_can_unlock_it: 파일을 잠근 사용자만 잠금 해제를 할 수 있습니다 + + error_max_files_exceeded: "동시에 다운 받을 수 있는 파일은 %{number}개로 제한되어 있습니다" + error_entry_project_does_not_match_current_project: 입력하신 프로젝트는 현재 프로젝트와 일치하지 않습니다 + + notice_folder_created: 폴더 생성됨 + error_folder_creation_failed: 폴더 생성 실패 + error_folder_title_must_be_entered: 제목을 입력해야 합니다 + notice_folder_deleted: 폴더 삭제됨 + error_folder_title_is_already_used: 이미 사용 중인 제목입니다 + notice_folder_details_were_saved: 폴더 상세 정보가 저장되었습니다 + error_folder_is_locked: 폴더가 잠겨있습니다 + error_file_is_locked: 파일이 잠겨있습니다 + notice_file_deleted: 파일 삭제 알림 + error_at_least_one_revision_must_be_present: 적어도 하나의 리비전이 있어야 합니다 + notice_revision_deleted: 리비전 삭제 알림 + notice_revision_obsoleted: 리비전 폐기 알림 + warning_one_of_files_locked: 잠겨있는 파일이 있습니다 + notice_file_revision_created: 리비전이 생성되었습니다 + notice_your_preferences_were_saved: 사용자의 선호도가 저장되었습니다 + notice_your_preferences_were_not_saved: 사용자의 선호도는 저장되지 않았습니다 + warning_folder_notifications_already_activated: 폴더 알림이 이미 활성화되어 있습니다 + + notice_folder_notifications_activated: 폴더 알림이 활성화되었습니다 + warning_folder_notifications_already_deactivated: 폴더 알림이 이미 비활성화 되었습니다 + + notice_folder_notifications_deactivated: 폴더 알림이 비활성화 되었습니다 + warning_file_notifications_already_activated: 파일 알림이 이미 활성화 되었습니다 + + notice_file_notifications_activated: 파일 알림이 활성화 되었습니다 + warning_file_notifications_already_deactivated: 파일 알림이 이미 비활성화 되었습니다 + + notice_file_notifications_deactivated: 파일 알림이 비활성화 되었습니다 + link_details: "%{title} 상세 내역" + link_edit: "%{title} 수정" + link_create_folder: 폴더 작성 + link_title: 제목 + link_size: 크기 + link_modified: 수정됨 + link_ver: 버전 + link_author: 작성자 + title_check_for_zip_download_or_email: Zip, 다운로드 혹은 전자 우편 체크 + title_check_for_restore_or_delete: 복구되거나 삭제된 파일 체크 + + title_notifications_active_deactivate: "알림 활성화: 비활성화" + title_notifications_not_active_activate: "알림 비활성화: 활성화" + title_title_version_version_download: "%{title} 버전 %{version} 다운로드" + title_locked_by_user: "%{user} 에 의해 잠겨짐" + title_waiting_for_approval: 승인을 기다리는 중 + title_approved: 승인됨 + title_unlock_file: 다른 구성원들의 변경 허가 잠금 해제 + title_lock_file: 다른 구성원들이 변경할 수 없도록 잠금 + title_download_checked: Zip 압축하여 다운로드 + title_send_checked_by_email: 전자 우편에서 발송 체크 + link_user_preferences: DMS 프로젝트 선호도 + heading_send_documents_by_email: 전자 우편으로 문서 발송 + label_email_from: 보낸이 + label_email_to: 수신인 + label_email_cc: 참조 + label_email_subject: 제목 + label_email_documents: 문서 + label_email_body: 본문 + label_email_send: 발송 + title_notifications_active: 알림 활성화 + label_upload: 업로드 + heading_new_folder: 새로운 폴더 + label_title: 제목 + label_description: 설명 + submit_save: 저장 + info_file_locked: 파일 잠김! + label_notifications: 알림 + select_option_default: 기본값 + select_option_deactivated: 비활성화 + select_option_activated: 활성화 + label_title_format: 제목 포맷 + text_title_format: "다운로드한 문서 제목 포맷 (%t - 제목, %f - 파일, %d - 날짜, %v - 버전, %i - ID, %r - 수정사항). 예시: %t_%v" + + title_save_preferences: 선호 사항 저장 + heading_revisions: 수정 + title_download: 다운로드 + title_delete_revision: 수정 사항 삭제 + title_obsolete_revision: 폐기된 리비전 + label_created: 생성 + label_changed: 변경 + info_changed_by_user: "이(가) %{changed}에 발생함" + label_filename: 파일명 + label_mime: MIME + label_size: 크기 + heading_new_revision: 새로운 수정 + option_version_same: 동일한 + option_version_patch: Patch + option_version_minor: 중요하지 않은 + option_version_major: 중요한 + option_version_custom: 임의의 + label_new_content: 새로운 콘텐츠 + label_maximum_files_download: 최대 다운로드 개수 + note_maximum_number_of_files_downloaded: Zip 혹은 전자 우편을 통해 다운로드 할 수 있는 최대 파일 수 제한. 0은 무제한을 나타냅니다. + + label_file_storage_directory: 파일 저장 디렉터리 + label_index_database: 데이터베이스 색인 + label_stemming_language: 형태소 분석 언어 + note_possible_values: 가능한 값 + note_pass_none_to_disable_stemming: "형태소 분석을 비활성화하기 위해선 '없음'을 선택" + label_stem_strategy: 형태소 분석 전략 + option_stem_none: 없음 (기본값) + option_stem_some: 부분적 + option_stem_all: 모두 + text_stemming_info: "이 설정은 질의 해석기가 어떻게 형태소 분석 알고리즘을 적용할 것인지를 선택합니다. 기본값은 '없음'입니다. + 가능한 값들은 다음과 같습니다: + 없음 - 형태소 분석을 수행하지 않습니다, + 부분적 - 대문자로 시작하거나 특정 문자(현재:'/@<>=*[{\"')가 뒤 따르거나 위치 정보가 필요한 연산자와 함께 사용되는 단어를 제외하고 형태소 분석을 합니다. 형태소 양식 앞에는 'Z' 접두어가 붙습니다, + 모두 - 모든 단어에 대해 형태소 분석을 합니다 (참고: 'Z' 접두어가 붙지 않음)." + label_default_notifications: 파일 기본 알림 + heading_uploaded_files: 업로드 된 파일 + link_documents: 문서 + permission_view_dmsf_file_revision_accesses: 활동 스트림에서 다운로드 확인하기 + permission_view_dmsf_file_revisions: 활동 스트림에서 수정사항 확인하기 + permission_view_dmsf_folders: 문서 검색 + permission_user_preferences: 사용자 선호 + permission_view_dmsf_files: 문서 보기 + permission_folder_manipulation: 폴더 변경 + permission_file_manipulation: 파일 변경 + permission_force_file_unlock: 강제로 파일 잠금 해제 + permission_manage_workflows: 작업 흐름 관리 + permission_file_delete: 문서 삭제 + permission_display_system_folders: 시스템 폴더를 표시 + permission_file_approval: 승인된 파일 + permission_email_documents: 전자 우편 전송 + label_file: 파일 + field_folder: 폴더 + error_file_commit_require_uploaded_file: 파일은 업로드 파일이 필요합니다 + + warning_some_files_were_not_committed: "다음 오류로 인하여 파일을 실행할 수 없습니다: %{files}" + + error_user_has_not_right_delete_folder: 사용자는 폴더를 삭제할 수 없습니다 + + error_user_has_not_right_delete_file: 사용자는 파일을 삭제할 수 없습니다 + + notice_entries_deleted: 항목이 삭제됨 + warning_some_entries_were_not_deleted: "일부 항목을 삭제할 수 없습니다: %{entries}" + title_delete_checked: 선택된 항목 삭제 + title_items: 항목 + title_filename_for_download: 다운로드 혹은 Zip 저장 공간을 위해 사용된 파일명 + label_number_of_folders: 폴더 + label_number_of_documents: 문서 + error_file_storage_directory_does_not_exist: 파일 저장 공간이 존재하지 않거나 생성되지 못했습니다 + + error_file_can_not_be_created: 저장 공간에 파일을 생성할 수 없습니다 + error_wrong_zip_encoding: 올바르지 않은 Zip 인코딩 + warning_xapian_not_available: Xapian을 사용할 수 없습니다 + menu_dmsf: DMS + label_physical_file_delete: 파일 삭제됨 + user_is_not_project_member: 이 프로젝트의 구성원이 아닙니다 + heading_access_downloads_emails: 다운로드/전자 우편 + heading_access_first: 처음 + heading_access_last: 마지막 + label_dmsf_updated: 업로드 됨 + label_dmsf_downloaded: 다운로드 됨 + title_total_size_of_all_files: 이 폴더의 모든 파일 총 크기 + project_module_dmsf: DMS + warning_no_project_to_copy_file_to: 로 파일이 복사된 프로젝트가 없습니다 + comment_copied_from: "%{source}에서 복사됨" + field_target_project: 대상 프로젝트 + field_target_folder: 대상 폴더 + title_copy_or_move: 복사/이동 + label_dmsf_folder_plural: 폴더 + comment_moved_from: "%{source}에서 이동됨" + error_target_folder_same: 대상 프로젝트와 폴더가 동일합니다 + title_copy: 복사 + + error_max_email_filesize_exceeded: "전자 우편을 이용해 발송할 수 있는 최대 파일 크기를 초과했습니다. (%{number} MiB)" + + note_maximum_email_filesize: 전자 우편을 통해 발송할 수 있는 최대 파일 크기. 0는 무제한을 의미합니다. 숫자는 MiB 단위입니다. + + label_maximum_email_filesize: 최대 전자 우편 첨부 파일 크기 + header_minimum_filesize: 파일 오류. + error_minimum_filesize: "%{file} 파일은 0 바이트이며 첨부할 수 없습니다." + parent_directory: 상위 디렉토리 + note_webdav: "활성화된 WebDAV는 %{protocol}에서 확인할 수 있습니다://%{domain}/dmsf/webdav/[project identifier]" + + label_copy_dmsf: "문서 및 폴더 복사 (%{folders} 폴더의 %{files} 파일들)" + label_copy_only_dmsf_folders: "문서 및 폴더 복사 (%{folders})" + + warning_folder_already_locked: 이 폴더는 이미 잠겨있습니다 + notice_folder_locked: 이 폴더는 성공적으로 잠겨있습니다 + warning_folder_not_locked: 안타깝게도 이 폴더를 잠글 수 없습니다 + notice_folder_unlocked: 이 폴더를 성공적으로 잠갔습니다 + error_only_user_that_locked_folder_can_unlock_it: 이 폴더를 잠금 해제할 수 없습니다 + + title_unlock_folder: 다른 회원이 변경할 수 있도록 잠금 해제 + title_lock_folder: 다른 회원이 변경할 수 없도록 잠금 + + select_option_webdav_readonly: 읽기 전용 + select_option_webdav_readwrite: 읽기/쓰기 + label_webdav_strategy: WebDAV 권한 + + note_webdav_strategy: 관리자가 WebDAV 최종 사용자를 위한 권한(읽기 전용 혹은 읽기/쓰기)을 결정할 수 있습니다. + + + error_unable_delete_dmsf_workflow: 작업 흐름 삭제 불가 + error_empty_note: 이 노트에 내용을 추가하세요 + error_workflow_assign: 지정하는 동안 오류가 발생했습니다 + error_cannot_start_workflow: 작업 흐름이 시작될 수 없습니다 + error_cannot_renumber_steps: 단계 수를 조정할 수 없습니다 + label_dmsf_workflow_new: 새로운 승인 절차 + field_label_dmsf_workflow: 승인 절차 + field_label_dmsf_workflow_name: 승인 절차 이름 + label_dmsf_workflow_plural: 승인 절차 + label_dmsf_workflow_plural_num: "승인 절차 (%{count})" + label_dmsf_workflow_step: 단계 + label_dmsf_workflow_step_plural: 단계 + label_dmsf_workflow_approval_plural: 승인 + label_dmsf_wokflow_action_approve: 승인 + label_dmsf_wokflow_action_reject: 반려 + label_dmsf_wokflow_action_delegate: 위임됨 + label_dmsf_wokflow_action_assign: 승인 절차 할당하기 + label_dmsf_wokflow_action_start: 승인 절차 시작하기 + label_dmsf_workflow_add_approver: 새로운 승인자 추가 + label_or: 혹은 + label_action: 활동 + label_note: 비고 + title_none: 없음 + title_rejection: 거절 + title_delegation: 위임 + title_assignment: 할당 + title_start: 시작 + title_dmsf_workflow_log: 승인된 절차 내역 + title_assigned: 할당됨 + title_approval: 승인 + title_rejected: 반려 + title_obsolete: 폐기 + dmsf_and: 그리고 + dmsf_or: 또는 + dmsf_new_step: 새로운 단계 + dmsf_new_step_or_approver: 새로운 단계 혹은 새로운 승인자 + message_dmsf_wokflow_note: 사용자의 노트... + info_revision: "r %{rev}" + link_workflow: 작업 흐름 + notice_workflow_started: 승인된 작업 흐름이 성공적으로 시작되었습니다 + text_email_subject_approved: 승인됨 + text_email_subject_rejected: 반려됨 + text_email_subject_delegated: 위임됨 + text_email_subject_requires_approval: 사용자의 승인이 필요합니다 + text_email_subject_updated: 업데이트 됨 + text_email_subject_started: 시작됨 + text_email_finished_approved: "'%{filename}' 문서에 지정된 승인 작업 흐름 '%{name}'이 방금 완료되었으며 문서가 승인되었습니다." + + text_email_finished_rejected: "'%{filename}' 문서에 지정된 승인 작업 흐름 '%{name}'이 방금 완료되었지만, '%{notice}'로 인해 문서가 반려되었습니다." + + text_email_finished_delegated: "'%{filename}' 문서에 지정된 승인 작업 흐름 '%{name}'이 방금 완료되었지만, '%{notice}'로 인해 위임되었으며, 사용자는 현재 승인 과정에서 승인하셔야 합니다 '%{stepname}'." + + text_email_finished_step: "'%{filename}' 문서에 지정된 승인 작업 흐름 '%{name}'이 방금 완료되었지만, 승인 단계 중 한 개가 승인되었으며 사용자께서 다음 승인 단계를 승인하셔야 합니다" + + text_email_finished_step_short: "'%{filename}'문서에 지정된 승인 작업 흐름 '%{name}'이 방금 완료되었으며, 승인 단계 중 한 단계가 완료되었습니다." + + text_email_started: "'%{filename}'문서에 지정된 승인 작업 흐름 '%{name}'이 방금 시작되었으며, 사용자께서는 현재 승인 단계에서 승인을 하셔야 합니다 '%{stepname}'." + + text_email_to_proceed: 다음으로 진행하기 위해 문서 옆에 있는 체크 상지 아이콘을 클릭하세요 + text_email_to_see_history: 승인 내역을 확인하기 위해 작업 흐름 상태 문서를 클릭하세요 + + text_email_to_see_status: 승인된 작업 흐름 상태를 확인하기 위해 작업 흐름 상태를 클릭하세요 + + + title_create_link: 링크 만들기 + label_link_from: 링크 만들기 + label_link_to: 여기로 오는 링크 만들기 + label_notifications_on: 알림 켜기 + label_notifications_off: 알림 끄기 + field_target_file: 소스 파일 + title_download_entries: 다운로드 엔트리 + label_external: 외부 + label_internal: 내부 + + label_link_name: 링크 이름 + field_external_url: URL + label_target_folder: 대상 폴더 + label_source_folder: 원본 폴더 + label_target_project: 대상 프로젝트 + label_source_project: 원본 프로젝트 + + text_email_doc_updated_subject: 문서 업데이트됨 + text_email_doc_updated: 문서가 막 업데이트 됨 + text_email_doc_follows: 다음과 같이 + text_email_doc_deleted_subject: 문서 삭제됨 + text_email_doc_deleted: 막 문서가 삭제됨 + label_links_only: 링크만 + + label_display_notified_recipients: 알림 수신인 보기 + note_display_notified_recipients: 사용자는 모든 수신자에게 전자 우편 알림을 발송합니다 + + warning_email_notifications: "%{to}에게 전자 우편 알림 발송됨" + + link_trash_bin: 휴지통 + title_restore: 복구 + notice_dmsf_file_restored: 문서가 성공적으로 복구되었습니다 + notice_dmsf_folder_restored: 폴더가 성공적으로 복구되었습니다 + notice_dmsf_link_restored: 링크가 성공적으로 복구되었습니다 + title_restore_checked: 복구 선택됨 + error_parent_folder: 상위 폴더가 존재하지 않습니다 + + error_resource_or_parent_locked: 잠금을 완료할 수 없음 - 리소스(혹은 상위 파일)가 잠겨 있음 + error_parent_locked: 잠금을 완료할 수 없음 - 리소스 상위 파일이 잠겨 있음 + error_resource_locked: 잠금 완료할 수 없음 - 리소스가 잠겨 있음 + error_lock_exclusively: 이미 잠겨있는 리소스를 잠글 수 없습니다 + error_unlock_parent_locked: 잠금 실패 - 리소스 상위 파일이 잠겨있습니다 + + label_dmsf_version: 버전 + + locked_documents: 잠긴 문서 + open_approvals: 승인 열기 + watched_documents: Watched documents + + error_maximum_upload_filecount: "%{filecount}가 넘는 파일을 업로드 할 수 없습니다." + + label_public_urls: 유효한 공개 URL + + label_webdav: WebDAV + label_full_text: 전문 검색 + link_extension: 확장자 + + label_webdav_ignore: 무시되는 파일 패턴 + note_webdav_ignore: PUT 요청에서 무시할 파일 이름의 정규 표현식입니다. + + label_document_url: URL + label_last_revision_id: 리비전 + + label_webdav_disable_versioning: 버전 관리되지 않는 파일 패턴 + note_webdav_disable_versioning: 일치하는 파일에 대해 버전 관리를 비활성화하는 정규 표현식입니다. 기본 패턴은 MS 오피스 소프트웨어에서 생성하는 임시 파일들에 일치합니다. + + + label_dmsf_keep_documents_locked: 문서를 잠긴 상태로 유지 + note_dmsf_keep_documents_locked: 문서가 승인될 때 잠긴 상태로 유지됩니다 + note_global: (전역) + field_dmsf_not_inheritable: 상속할 수 없는 + + label_webdav_use_project_names: 프로젝트 폴더에 대해 프로젝트 이름을 사용 + note_webdav_use_project_names: 프로젝트 폴더에 대해 식별자 대신에 이름을 사용합니다. + + label_last_approver: 최종 승인 + + label_act_as_attachable: 첨부 가능 + note_dmsf_act_as_attachable: 일감 등의 객체에 문서를 첨부하는 것을 허용합니다. + + label_user_search_add: 추가할 사용자 검색 + + label_dmsf_attachments: DMS 첨부 + label_basic_attachments: 기본 첨부 + + label_email_from_override: 송신 + text_email_from_override: 로그인한 사용자 + 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" + + + + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: 내용 편집 + field_workflow: 작업 흐름 + field_modified: 수정 날짜 + field_updated: 수정 날짜 + field_count: 다운로드 + field_first_at: 처음 + field_last_at: 마지막 + field_size: 크기 + field_locked: Locked + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: 제목 + label_column_size: 크기 + label_column_modified: 변경됨 + label_column_version: 버전 + label_column_workflow: 업무흐름 + label_column_author: 저자 + label_column_description: 설명 + label_column_comment: Comment + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: 사용이 불가능한 문자들을 포함 diff --git a/config/locales/nl.yml b/config/locales/nl.yml new file mode 100644 index 00000000..b9dd3eb7 --- /dev/null +++ b/config/locales/nl.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +nl: + dmsf: DMS + label_dmsf_file: DMS Document + label_dmsf_file_plural: Documenten + label_dmsf_file_revision_plural: Document revisies + label_dmsf_file_revision_access_plural: Document toegang + warning_no_entries_selected: Geen invoer geselecteerd + error_email_to_must_be_entered: E-mail Naar moet worden ingevuld + warning_file_already_locked: Bestand al vergrendeld + notice_file_locked: Bestand vergrendeld + warning_file_not_locked: Bestand niet vergrendeld + notice_file_unlocked: Bestand vergrendeld + error_only_user_that_locked_file_can_unlock_it: Alleen de gebruiker die het bestand heeft vergrendeld kan het + ontgrendelen + error_max_files_exceeded: "Limiet voor %{number} tegelijk gedownloade bestanden overschreden" + error_entry_project_does_not_match_current_project: Invoer project is niet gelijk aan huidig project + + notice_folder_created: Map aangemaakt + error_folder_creation_failed: Map aanmaken mislukt + error_folder_title_must_be_entered: Titel moet worden ingevoerd + notice_folder_deleted: Map verwijderd + error_folder_title_is_already_used: Titel is al in gebruik + notice_folder_details_were_saved: Gegevens map zijn opgeslagen + error_folder_is_locked: Map is vergrendeld + error_file_is_locked: Map is vergrendeld + notice_file_deleted: Map verwijderd + error_at_least_one_revision_must_be_present: Er moet ten minste een revisie aanwezig zijn + notice_revision_deleted: Revisie verwijderd + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: Een van de bestanden vergrendeld + notice_file_revision_created: Bestandsrevisie aangemaakt + notice_your_preferences_were_saved: Uw voorkeuren zijn opgeslagen + notice_your_preferences_were_not_saved: Uw voorkeuren zijn niet opgeslagen + warning_folder_notifications_already_activated: Meldingen map zijn al geactiveerd + + notice_folder_notifications_activated: Meldingen map geactiveerd + warning_folder_notifications_already_deactivated: Meldingen map al gedeactiveerd + + notice_folder_notifications_deactivated: Meldingen map gedeactiveerd + warning_file_notifications_already_activated: Meldingen bestand al geactiveerd + + notice_file_notifications_activated: Meldingen bestand geactiveerd + warning_file_notifications_already_deactivated: Meldingen bestand al gedeactiveerd + + notice_file_notifications_deactivated: Meldingen bestand gedeactiveerd + link_details: "%{title} details" + link_edit: "Aanpassen %{title}" + link_create_folder: Maak map + link_title: Titel + link_size: Bestandsgrootte + link_modified: Aangepast + link_ver: Ver. + link_author: Auteur + title_check_for_zip_download_or_email: Vink voor zip, download of email + title_check_for_restore_or_delete: Vink voor herstel of verwijder + + title_notifications_active_deactivate: "Notificaties actief: Deactiveren" + title_notifications_not_active_activate: "Notificaties niet actief: Activeren" + title_title_version_version_download: "%{title} versie %{version} downloaden" + title_locked_by_user: "Vergrendeld door %{user}" + title_waiting_for_approval: Wacht op goedkeuring + title_approved: Goedgekeurd + title_unlock_file: Ontgrendel om wijzigingen door andere leden toe te staan + title_lock_file: Vergrendel om wijzigingen door andere leden te voorkomen + title_download_checked: Download aangevinkt in Zip archief + title_send_checked_by_email: Verzending aangevinkt door e-mail + link_user_preferences: DMS project voorkeuren + heading_send_documents_by_email: Verstuur documenten per e-mail + label_email_from: Van + label_email_to: Aan + label_email_cc: CC + label_email_subject: Onderwerp + label_email_documents: Documenten + label_email_body: Body + label_email_send: Verzenden + title_notifications_active: Meldingen actief + label_upload: Uploaden + heading_new_folder: Nieuwe Map + label_title: Titel + label_description: Beschrijving + submit_save: Opslaan + info_file_locked: Bestand vergrendeld + label_notifications: Meldingen + select_option_default: Standaard + select_option_deactivated: Gedeactiveerd + select_option_activated: Geactiveerd + label_title_format: Titel format + text_title_format: "Document titel format voor download (%t - title, %f - file, %d - date, %v - version, %i - ID, + %r - revision). Voorbeeld: %t_%v" + title_save_preferences: Sla voorkeuren op + heading_revisions: Revisies + title_download: Downloaden + title_delete_revision: Verwijder revisie + title_obsolete_revision: Obsolete revision + label_created: Aangemaakt + label_changed: Gewijzigd + info_changed_by_user: "%{changed} door" + label_filename: Bestandsnaam + label_mime: Mime + label_size: Grootte + heading_new_revision: Nieuwe Revisie + option_version_same: Hetzelfde + option_version_patch: Patch + option_version_minor: Beperkt + option_version_major: Belangrijk + option_version_custom: Aangepast + label_new_content: Nieuwe inhoud + label_maximum_files_download: Maximaal aantal bestanden download + note_maximum_number_of_files_downloaded: Beperkt maximaal aantal bestanden gedownload in zip of verzonden via e-mail. + 0 betekent onbeperkt. + label_file_storage_directory: Overzicht bestandenopslag + label_index_database: Index database + label_stemming_language: Stemming Language + note_possible_values: Mogelijke waardes + note_pass_none_to_disable_stemming: "geef 'geen' in om stemming uit te schakelen" + label_stem_strategy: Stem strategie + option_stem_none: Stem geen (standaard) + option_stem_some: Stem sommige + option_stem_all: Stem alle + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: Standaard bestand notificaties + heading_uploaded_files: Geuploade Bestanden + link_documents: Documenten + permission_view_dmsf_file_revision_accesses: Bekijk downloads in Activiteiten stroom + permission_view_dmsf_file_revisions: Bekijk revisies in Activiteiten stroom + permission_view_dmsf_folders: Doorzoek documenten + permission_user_preferences: Gebruikersvoorkeuren + permission_view_dmsf_files: Bekijk documenten + permission_folder_manipulation: Map manipulatie + permission_file_manipulation: Bestand manipulatie + permission_force_file_unlock: Dwing ontrendeling bestand + permission_manage_workflows: Beheer workflows + permission_file_delete: Verwijder documenten + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: Bestand + field_folder: Map + error_file_commit_require_uploaded_file: Bestand indienen vereist geüpload bestand + + warning_some_files_were_not_committed: Sommige bestanden zijn niet ingediend door + + error_user_has_not_right_delete_folder: Gebruiker heeft geen recht om mappen te verwijderen + + error_user_has_not_right_delete_file: Gebruiker heeft geen recht om bestand te verwijderen + + notice_entries_deleted: Inzendingen verwijderd + warning_some_entries_were_not_deleted: "Enkele inzendingen zijn niet verwijderd: %{entries}" + title_delete_checked: Verwijdering gecontroleerd + title_items: items + title_filename_for_download: Bestandsnaam gebruikt voor download of in Zip archief + label_number_of_folders: Mappen + label_number_of_documents: Documenten + error_file_storage_directory_does_not_exist: Bestandsopslag directory bestaat niet en kan niet aangemaakt worden + + error_file_can_not_be_created: Bestand kan niet aangemaakt worden in opslag directory + error_wrong_zip_encoding: Onjuiste Zip encoding + warning_xapian_not_available: Xapian niet beschikbaar + menu_dmsf: DMS + label_physical_file_delete: Fysiek bestand verwijderen + user_is_not_project_member: U bent geen lid van het project + heading_access_downloads_emails: Downloads/E-mails + heading_access_first: Eerst + heading_access_last: Laatst + label_dmsf_updated: Bijgewerkt + label_dmsf_downloaded: Gedownload + title_total_size_of_all_files: Totale grootte van alle bestanden onder deze map + project_module_dmsf: DMS + warning_no_project_to_copy_file_to: Geen project om bestand naar te kopiëren + comment_copied_from: "Gekopieerd van %{source}" + field_target_project: Doelproject + field_target_folder: Doelmap + title_copy_or_move: Kopieer/Verplaats + label_dmsf_folder_plural: Mappen # Search options + comment_moved_from: "Verplaatst van %{source}" + error_target_folder_same: Doelmap en project zijn hetzelfde als huidige + title_copy: Kopieer + + error_max_email_filesize_exceeded: "U heeft de maximale bestandsgrootte voor verzending via e-mail overschreden. + (%{number} MB)" + note_maximum_email_filesize: Beperkt maximale bestandsgrootte die verzonden kan worden via e-mail. 0 betekent + onbeperkt. Getal is in MB. + label_maximum_email_filesize: Maximale grootte e-mail bijlage + header_minimum_filesize: Bestandsfout. + error_minimum_filesize: "Het bestand %{file} is 0 bytes en wordt niet bijgevoegd." + parent_directory: Ouder directory + note_webdav: "Webdav kan indien ingeschakeld worden gevonden op %{protocol}://%{domain}/dmsf/webdav/[project + identifier]" + label_copy_dmsf: "Kopieer documenten en mappen (%{files} bestanden in %{folders} mappen)" + label_copy_only_dmsf_folders: "Kopieer mappen (%{folders})" + + warning_folder_already_locked: Deze map is al vergrendeld + notice_folder_locked: Deze map is succesvol vergrendeld + warning_folder_not_locked: Helaas kon deze map niet vergrendeld worden + notice_folder_unlocked: De map is succesvol vergrendeld + error_only_user_that_locked_folder_can_unlock_it: U bent niet geautoriseerd om deze map te ontgrendelen + + title_unlock_folder: Ontgrendel om wijzigingen door andere gebruikers toe te staan + title_lock_folder: Vergrendel om wijzigingen door andere leden te voorkomen + + select_option_webdav_readonly: Alleen-lezen + select_option_webdav_readwrite: Lezen/Schrijven + label_webdav_strategy: Webdav strategie + + note_webdav_strategy: Staat de administrator toe om te beslissen of webdav een alleen-lezen of lezen-schrijven + platform is voor eindgebruikers. + + error_unable_delete_dmsf_workflow: Kon workflow niet verwijderen + error_empty_note: De notitie kan niet leeg zijn + error_workflow_assign: Er is een fout opgetreden bij het toewijzen + error_cannot_start_workflow: Workflow kan niet gestart worden + error_cannot_renumber_steps: Stappen kunnen niet opnieuw genummerd worden + label_dmsf_workflow_new: Nieuwe goedkeuring workflow + field_label_dmsf_workflow: Goedkeuring Workflow + field_label_dmsf_workflow_name: Goedkeuring workflow naam + label_dmsf_workflow_plural: Goedkeuring workflows + label_dmsf_workflow_plural_num: "Goedkeuring workflows (%{count})" + label_dmsf_workflow_step: Stap + label_dmsf_workflow_step_plural: Stappen + label_dmsf_workflow_approval_plural: Goedkeuringen + label_dmsf_wokflow_action_approve: Goedkeuren + label_dmsf_wokflow_action_reject: Afwijzen + label_dmsf_wokflow_action_delegate: Toewijzen aan + label_dmsf_wokflow_action_assign: Een goedkeuring workflow toewijzen + label_dmsf_wokflow_action_start: Start workflow + label_dmsf_workflow_add_approver: Voeg een nieuwe goedkeurder toe moet een logische functie + label_or: of + label_action: Actie + label_note: Notitie + title_none: Geen + title_rejection: Afwijzing + title_delegation: Delegatie + title_assignment: Toewijzing + title_start: Start + title_dmsf_workflow_log: Goedkeuring Workflow Log + title_assigned: Toegewezen + title_approval: Goedkeuring + title_rejected: Afgewezen + title_obsolete: Obsolete + dmsf_and: EN + dmsf_or: OF + dmsf_new_step: Nieuwe stap + dmsf_new_step_or_approver: Nieuw stap of Nieuwe goedkeurder + message_dmsf_wokflow_note: Uw notitie... + info_revision: "r %{rev}" + link_workflow: Workflow + notice_workflow_started: Goedkeuring workflow succesvol gestart + text_email_subject_approved: goedgekeurd + text_email_subject_rejected: afgewezen + text_email_subject_delegated: gedelegeerd + text_email_subject_requires_approval: vereist uw goedkeuring + text_email_subject_updated: bijgewerkt + text_email_subject_started: gestart + text_email_finished_approved: "De goedkeuring workflow '%{name}' toegewezen aan '%{filename}' document is zojuist + afgerond en het document is goedgekeurd." + text_email_finished_rejected: "De goedkeuring workflow '%{name}' toegewezen aan '%{filename}' document is zojuist + afgerond en het document is afgewezen vanwege '%{notice}'." + text_email_finished_delegated: "De goedkeuring workflow '%{name}' toegewezen aan '%{filename}' document is zojuist + gedelegeerd omdat '%{notice}' en er wordt verwacht dat u een goedkeuring doet in de huidige goedkeuringsstap '%{stepname}'." + text_email_finished_step: "De goedkeuring workflow '%{name}' toegewezen aan '%{filename}' document heeft zojuist een + van de goedkeurende stappen afgerond en er wordt verwacht dat u een goedkeuring doet in de volgenden + goedkeuringsstap." + text_email_finished_step_short: "De goedkeuring workflow '%{name}' toegewezen aan '%{filename}' document heeft zojuist + een van de goedkeuringsstappen afgerond." + text_email_started: "De goedkeurende workflow '%{name}' toegewezen aan '%{filename}' document is zojuist gestart en er + wordt verwacht dat u een goedkeuring doet in de huidige goedkeuringsstap '%{stepname}'." + text_email_to_proceed: Klik om door te gaan om het het vinkjes icoon naast het document in + text_email_to_see_history: Klik om de goedkeuringsgeschiedenis te bekijken op de workflow status van het document in + text_email_to_see_status: Klik om de huidige status van de goedkeuring workflow te bekijken op het workflow status + document in + + title_create_link: Maak een symbolische link + label_link_from: Link van + label_link_to: Link naar + label_notifications_on: Notificaties AAN + label_notifications_off: Notificaties UIT + field_target_file: Bronbestand + title_download_entries: Download indieningen + label_external: Extern + label_internal: Internal + + label_link_name: Link naam + field_external_url: URL + label_target_folder: Doelmap + label_source_folder: Bronmap + label_target_project: Doelproject + label_source_project: Bronproject + + text_email_doc_updated_subject: Bijgewerkte documenten + text_email_doc_updated: heeft zojuist de documenten bijgewerkt van + text_email_doc_follows: als volgt + text_email_doc_deleted_subject: Documenten verwijderd + text_email_doc_deleted: heeft zojuist documenten verwijderd van + label_links_only: alleen links + + label_display_notified_recipients: Toon genotificeerde ontvangers + note_display_notified_recipients: De gebruiker wordt geïnformeerd over alle ontvangers van de verzonden e-mail + notificatie. + warning_email_notifications: "E-mail notificaties verzonden naar %{to}" + + link_trash_bin: Prullenmand + title_restore: Herstel + notice_dmsf_file_restored: Het document is succesvol hersteld + notice_dmsf_folder_restored: De map is succesvol hersteld + notice_dmsf_link_restored: De link is succesvol hersteld + title_restore_checked: Herstel gecontroleerd + error_parent_folder: De ouder map bestaat niet + + error_resource_or_parent_locked: Vergrendelen niet afgerond - bron (of ouder) is vergrendeld + error_parent_locked: Vergrendelen niet afgerond - bron ouder is vergrendeld + error_resource_locked: Vergrendelen niet afgerond - bron is vergrendeld + error_lock_exclusively: Vergrendelen niet mogelijk op een reeds vergrendelde bron + error_unlock_parent_locked: Ontgrendelen mislukt - bron ouder is vergrendeld + + label_dmsf_version: Versie + + locked_documents: Vergrendelde documenten + open_approvals: Open goedkeuringen + watched_documents: Watched documents + + error_maximum_upload_filecount: "Er kunnen niet meer dan %{filecount} bestand(en) worden geupload." + + label_public_urls: Public URLs valid to + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Ignored files patterns + note_webdav_ignore: A regular expresion with filenames to ignore by PUT requests. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. + + + label_dmsf_keep_documents_locked: Keep documents locked + note_dmsf_keep_documents_locked: Documents will be kept locked when approved + note_global: (global) + field_dmsf_not_inheritable: Not inheritable + + label_webdav_use_project_names: Use project name for project folder + note_webdav_use_project_names: Use project names instead of project identifier for project folders. + + label_last_approver: Last approver + + label_act_as_attachable: Act as attachable + note_dmsf_act_as_attachable: Allows to attach documents to objects e.g. issues. + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: Workflow + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: Grootte + field_locked: Vergrendeld + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: Titel + label_column_size: Grootte + label_column_modified: Gewijzigd + label_column_version: Versie + label_column_workflow: Workflow + label_column_author: Auteur + label_column_description: Beschrijving + label_column_comment: Commentaar + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: bevat ongeldige teken(s) \ No newline at end of file diff --git a/config/locales/pl.yml b/config/locales/pl.yml new file mode 100644 index 00000000..b8dd78cf --- /dev/null +++ b/config/locales/pl.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# Polish translation created by Sebastian Białas www.bs-it.pl +# +# 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 +# . + +pl: + dmsf: DMS # Custom fields tab title + label_dmsf_file_plural: DMS Pliki # Email subject & Search options + label_dmsf_file_revision_plural: Document revisions + label_dmsf_file_revision_access_plural: Document accesses + warning_no_entries_selected: Nie zaznaczono żadnych wierszy + error_email_to_must_be_entered: Musisz podać adres email + warning_file_already_locked: Plik jest już zablokowany + notice_file_locked: Plik zablokowany + warning_file_not_locked: Plik nie zablokowany + notice_file_unlocked: Plik odblokowany + error_only_user_that_locked_file_can_unlock_it: Plik może zostać odblokowany jedynie przez użytkownika, który go + zablokował + error_max_files_exceeded: "Limit %{number} jedocześnie pobieranych plików został przekroczony" + error_entry_project_does_not_match_current_project: "Podany projekt nie odpowiada obecnemu projektowy" + + notice_folder_created: Folder został utworzony + error_folder_creation_failed: Błąd podczas tworzenia folderu + error_folder_title_must_be_entered: Musisz podać tytuł + notice_folder_deleted: Folder został usunięty + error_folder_title_is_already_used: Podany tytuł jest już w użyciu + notice_folder_details_were_saved: Szczegóły folderu zostały zapisane + error_folder_is_locked: Folder jest zablokowany + error_file_is_locked: Plik jest zablokowany + notice_file_deleted: Plik został usunięty + error_at_least_one_revision_must_be_present: Musi istnieć co najmniej jedna wersja + notice_revision_deleted: Wersja usunięta + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: Jeden z plików jest zablokowany + notice_file_revision_created: Utworzono wersję pliku + notice_your_preferences_were_saved: Twoje preferencje zostały zapisane + notice_your_preferences_were_not_saved: Your preferences were not saved + warning_folder_notifications_already_activated: Powiadomienia dla folderu zostały już aktywowane + + notice_folder_notifications_activated: Aktywowano powiadomienia dla folderu + warning_folder_notifications_already_deactivated: Powiadomienia dla folderu zostały już wyłączone + + notice_folder_notifications_deactivated: Wyłączono powiadomienia dla folderu + warning_file_notifications_already_activated: Powiadomienia dla pliku zostały już aktywowane + + notice_file_notifications_activated: Aktywowano powiadomienia dla pliku + warning_file_notifications_already_deactivated: Powiadomienia dla pliku zostały już wyłączone + + notice_file_notifications_deactivated: Wyłączono powiadomienia dla pliku + link_details: "szczegóły %{title}" + link_edit: "Edytuj %{title}" + link_create_folder: Utwórz folder + link_title: Tytuł + link_size: Rozmiar + link_modified: Zmodyfikowany + link_ver: Ver. + link_author: Autor + title_check_for_zip_download_or_email: Zaznacz aby pobrać plik zip lub wysłać email + title_check_for_restore_or_delete: Check for restore or delete + + title_notifications_active_deactivate: "Powiadomienia aktywne: Wyłącz" + title_notifications_not_active_activate: "Powiadomienia wyłączone: Aktywuj" + title_title_version_version_download: "pobierz %{title} wersja %{version}" + title_locked_by_user: "Zablokowany przez %{user}" + title_waiting_for_approval: Oczekiwanie na akceptację + title_approved: Zaakceptowany + title_unlock_file: Odblokuj aby umożliwić wprowadzanie zmian innym użytkownikom + title_lock_file: Zablokuj aby zabezpieczyć przed wprowadzaniem zmian przez innych użytkowników + title_download_checked: Pobierz zaznaczone jako archiwum Zip + title_send_checked_by_email: Wyślij zaznaczone przez email + link_user_preferences: Preferencje DMS dla projektu + heading_send_documents_by_email: Wyślij dokumenty przez email + label_email_from: Od + label_email_to: Do + label_email_cc: CC + label_email_subject: Temat + label_email_documents: Dokumenty + label_email_body: Treść + label_email_send: Wyślij + title_notifications_active: Powiadomienia aktywne + label_upload: Prześlij + heading_new_folder: Nowy Folder + label_title: Tytuł + label_description: Opis + submit_save: Zapisz + info_file_locked: Plik zablokowany! + label_notifications: Powiadomienia + select_option_default: Domyślny + select_option_deactivated: Wyłączono + label_title_format: Title format + text_title_format: "Document title format for download (%t - title, %f - file, %d - date, %v - version, %i - ID, %r - + revision). Example: %t_%v" + select_option_activated: Aktywowano + title_save_preferences: Zapisz ustawienia + heading_revisions: Wersje + title_download: Pobierz + title_delete_revision: Usuń wersję + title_obsolete_revision: Obsolete revision + label_created: Utworzono + label_changed: Zmieniono + info_changed_by_user: "%{changed} przez" + label_filename: Nazwa pliku + label_mime: Mime + label_size: Rozmiar + heading_new_revision: Nowa wersja + option_version_same: Same + option_version_patch: Patch + option_version_minor: Minor + option_version_major: Major + option_version_custom: Custom + label_new_content: Nowa zawartość + label_maximum_files_download: Maksymalna liczba jednocześnie pobieranych plików + note_maximum_number_of_files_downloaded: Maksymalna liczba plików pobieranych w paczce zip lub wysyłanych przez email. + 0 oznacza brak ograniczeń. + label_file_storage_directory: Folder przechowywania plików + label_index_database: Index database + label_stemming_language: Stemming language + note_possible_values: Możliwe wartości + note_pass_none_to_disable_stemming: "pass 'none' to disable stemming" + label_stem_strategy: Stem strategy + option_stem_none: Stem none (default) + option_stem_some: Stem some + option_stem_all: Stem all + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: Domyślne powiadomienia plików + heading_uploaded_files: Przesłane pliki + 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_folders: Przeglądaj dokumenty + permission_user_preferences: Preferencje użytkownika + permission_view_dmsf_files: Podgląd dokumentów + permission_folder_manipulation: Modyfikacja folderów + permission_file_manipulation: Modyfikacja plików + permission_force_file_unlock: Wymuś odblokowanie pliku + permission_manage_workflows: Zarządzaj procesami + permission_file_delete: Usuń dokumenty + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: Plik + field_folder: Folder + error_file_commit_require_uploaded_file: Commit pliku wymaga jego uprzedniego przesłania + + warning_some_files_were_not_committed: "Niektóre pliki nie zostały zacommitowane ze względu na błędy walidacji: + %{files}" + error_user_has_not_right_delete_folder: "Użytkownik nie posiada uprawnień do usuwania folderów" + + error_user_has_not_right_delete_file: "Użytkownik nie posiada uprawnień do usuwania plików" + + notice_entries_deleted: Wpisy usunięte + warning_some_entries_were_not_deleted: "Niektóre wpisy nie zostały usunięte: %{entries}" + title_delete_checked: Usuń zaznaczone + title_items: items + title_filename_for_download: Nazwa pliku używana do pobierania lub tworzenai archiwum Zip + label_number_of_folders: Foldery + label_number_of_documents: Dokumenty + error_file_storage_directory_does_not_exist: "Folder przechowywania plików nie istnieje i nie może zostać utworzony" + + error_file_can_not_be_created: "Plik nie może zostać utworzony w folderze przechowywania" + error_wrong_zip_encoding: Złe kodowanie Zip + warning_xapian_not_available: Xapian niedostępny + menu_dmsf: DMS # Project tab title + label_physical_file_delete: Fizyczne usuwanie plików + user_is_not_project_member: Nie jesteś przypisany do tego projektu + heading_access_downloads_emails: Downloads/Emails + heading_access_first: Pierwszy + heading_access_last: Ostatni + label_dmsf_updated: Zaktualizowany + label_dmsf_downloaded: Downloaded + title_total_size_of_all_files: Łączny rozmiar plików w folderze + project_module_dmsf: DMS # Project module name + warning_no_project_to_copy_file_to: Brak projektu do skopiowania pliku + comment_copied_from: "Skopiowano z %{source}" + field_target_project: Projekt docelowy + field_target_folder: Folder docelowy + title_copy_or_move: Kopiuj/Przenieś + label_dmsf_folder_plural: DMS Foldery # Search options + comment_moved_from: "Przeniesiono z %{source}" + error_target_folder_same: Docelowy projekt i folder są identyczne z obecnym + title_copy: Kopiuj + + error_max_email_filesize_exceeded: "Maksymalny rozmiar pliku załącznika email został przekroczony. (%{number} MB)" + + note_maximum_email_filesize: Maksymalny rozmiar pliku, który może zostać wysłany przez email. 0 oznacza brak + ograniczeń. Rozmiar w MB. + label_maximum_email_filesize: Maksymalny rozmiar załącznika email + header_minimum_filesize: Błąd pliku. + error_minimum_filesize: "Plik %{file} ma 0 bytes i nie zostanie załączony." + parent_directory: Folder nadrzędny + note_webdav: "Webdav once enabled can be found at %{protocol}://%{domain}/dmsf/webdav/[project identifier]" + + label_copy_dmsf: "Skopiuj pliki i foldery (%{files} plików w %{folders} folderach)" + label_copy_only_dmsf_folders: "Skopiuj tylko foldery (%{folders})" + + warning_folder_already_locked: Folder jest już zablokowany + notice_folder_locked: Folder został zablokowany + warning_folder_not_locked: Niestety folder nie może zostać zablokowany + notice_folder_unlocked: Folder został odblokowany + error_only_user_that_locked_folder_can_unlock_it: Nie posiadasz uprawnień do odblokowania tego folderu + + title_unlock_folder: Odblokuj w celu umożliwienia wprowadzania zmian innym użytkownikom + title_lock_folder: Zablokuj aby zabezpieczyć przed wprowadzaniem zmian przez innych użytkowników + + select_option_webdav_readonly: Tylko do odczytu + select_option_webdav_readwrite: Odczyt/Zapis + label_webdav_strategy: Webdav strategy + + note_webdav_strategy: Enables the administrator to decide if webdav is a read-only or read-write platform for end + users. + + error_unable_delete_dmsf_workflow: Nie można usunąć procesu workflow + error_empty_note: "Notatka nie może być pusta" + error_workflow_assign: Wystąpił błąd podczas przydzielania + error_cannot_start_workflow: "Workflow nie może zostać uruchomiony" + error_cannot_renumber_steps: "Kroki nie mogą zostać przenumerowane" + label_dmsf_workflow_new: Nowy proces akceptacji + field_label_dmsf_workflow: Proces akceptacji + field_label_dmsf_workflow_name: Nazwa procesu akceptacji + label_dmsf_workflow_plural: Procesy akceptacji + label_dmsf_workflow_plural_num: Procesy akceptacji (%{count}) + label_dmsf_workflow_step: Krok + label_dmsf_workflow_step_plural: Kroki + label_dmsf_workflow_approval_plural: Akceptacje + label_dmsf_wokflow_action_approve: Zaakceptuj + label_dmsf_wokflow_action_reject: Odrzuć + label_dmsf_wokflow_action_delegate: Deleguj do + label_dmsf_wokflow_action_assign: Przydziel proces akceptacji + label_dmsf_wokflow_action_start: Uruchom workflow + label_dmsf_workflow_add_approver: "Dodaj nowego akceptującego z warunkiem logicznym:" + label_or: lub + label_action: Akcja + label_note: Notatka + title_none: Brak + title_rejection: Odrzucenie + title_delegation: Delegacja + title_assignment: Przydział + title_start: Start + title_dmsf_workflow_log: Historia procesu akceptacji + title_assigned: Przydzielony + title_approval: Akceptacja + title_rejected: Odrzucony + title_obsolete: Obsolete + dmsf_and: AND + dmsf_or: OR + dmsf_new_step: Nowy krok + dmsf_new_step_or_approver: Nowy krok lub Nowy akceptować + message_dmsf_wokflow_note: Twoja notatka... + info_revision: "r %{rev}" + link_workflow: Proces akceptacji + notice_workflow_started: Proces akceptacji został uruchomiony + text_email_subject_approved: został zakończony akceptacją + text_email_subject_rejected: został odrzucony + text_email_subject_delegated: został delegowany + text_email_subject_requires_approval: wymaga Twojej akceptacji + text_email_subject_updated: został zaktualizowany + text_email_subject_started: został uruchomiony + text_email_finished_approved: "Proces akceptacji '%{name}' dokumentu '%{filename}' został właśnie zakończony. + Dokument został zaakceptowany." + text_email_finished_rejected: "Proces akceptacji '%{name}' dokumentu '%{filename}' został właśnie zakończony. Dokument + został odrzucony z powodu '%{notice}'." + text_email_finished_delegated: "Proces akceptacji '%{name}' dokumentu '%{filename}' został właśnie delegowany z powodu + '%{notice}'. Zostałeś wskazany jako akceptujący w bieżącym kroku zatwierdzania '%{stepname}'." + text_email_finished_step: "Zakończono krok w procesie akceptacji '%{name}' dokumentu '%{filename}'. Jesteś kolejną + osobą decyzyjną w procesie akceptacji." + text_email_finished_step_short: "Zakończono krok w procesie akceptacji '%{name}' dokumentu '%{filename}'." + + text_email_started: "Proces akceptacji '%{name}' dokuentu '%{filename}' został uruchomiony. Jesteś osobą akceptującą w + bieżącym kroku zatwierdzania '%{stepname}'." + text_email_to_proceed: Aby procedować zaznacz check box przy dokumencie + text_email_to_see_history: Aby zobaczyć historię akceptacji kliknij w proces akceptacji dokumentu + + text_email_to_see_status: Aby zobaczyć aktualny stan procesu akceptacji kliknij w proces akceptacji dokumentu + + + title_create_link: Utwórz symbolic link + label_link_from: Odnośnik z + label_link_to: Odnośnik do + label_notifications_on: Powiadomienia włączone + label_notifications_off: Powiadomienia wyłączone + field_target_file: Plik źródłowy + title_download_entries: Pobrane + label_external: External + label_internal: Internal + + label_link_name: Nazwa odnośnika + field_external_url: URL + label_target_folder: Folder docelowy + label_source_folder: Folder źródłowy + label_target_project: Projekt docelowy + label_source_project: Projekt źródłowy + + text_email_doc_updated_subject: Dokumenty zostały zaktualizowane + text_email_doc_updated: dokumenty zostały zaktualizowane + text_email_doc_follows: następujące + text_email_doc_deleted_subject: Dokumenty zostały usunięte + text_email_doc_deleted: dokumenty zostały usunięte + label_links_only: odnośniki + + label_display_notified_recipients: Wyświetl odbiorców powiadomienia + note_display_notified_recipients: Użytkownik zostanie poinformowany o wszystkich odbiorcach wysłanego powiadomienia + email. + warning_email_notifications: "Powiadomienie email zostało wysłane do %{to}" + + link_trash_bin: Kosz + title_restore: Przywróć + notice_dmsf_file_restored: Dokument został przywrócony + notice_dmsf_folder_restored: Folder został przywrócony + notice_dmsf_link_restored: Link został przywrócony + title_restore_checked: Restore checked + error_parent_folder: "Folder nadrzędny nie istnieje" + + error_resource_or_parent_locked: Unable to complete lock - resource (or parent) is locked + error_parent_locked: Unable to complete lock - resource parent is locked + error_resource_locked: Unable to complete lock - resource is locked + error_lock_exclusively: Unable to lock exclusively an already-locked resource + error_unlock_parent_locked: Unlock failed - resource parent is locked + + label_dmsf_version: Wersja + + locked_documents: Dokumenty zablokowane + open_approvals: Otwarte procesy akceptacji + watched_documents: Watched documents + + error_maximum_upload_filecount: "No more than %{filecount} file(s) can be uploaded." + + label_public_urls: Public URLs valid to + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Ignored files patterns + note_webdav_ignore: A regular expresion with filenames to ignore by PUT requests. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. The default pattern + matches temporary files created by MsOffice. + + label_dmsf_keep_documents_locked: Keep documents locked + note_dmsf_keep_documents_locked: Documents will be kept locked when approved + note_global: (global) + field_dmsf_not_inheritable: Not inheritable + + label_webdav_use_project_names: Use project name for project folder + note_webdav_use_project_names: Use project names instead of project identifier for project folders. + + label_last_approver: Last approver + + label_act_as_attachable: Act as attachable + note_dmsf_act_as_attachable: Allows to attach documents to objects e.g. issues. + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: Akceptacje + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: Rozmiar + field_locked: Zablokowany + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: Tytuł + label_column_size: Rozmiar + label_column_modified: Zmodyfikowane + label_column_version: Wersja + label_column_workflow: Akceptacje + label_column_author: Autor + label_column_description: Opis + label_column_comment: Komentarz + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: zawiera nieprawidłowe znaki \ No newline at end of file diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml new file mode 100644 index 00000000..f6dcb30c --- /dev/null +++ b/config/locales/pt-BR.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +pt-BR: + dmsf: DMS # Custom fields tab title + label_dmsf_file: DMS Arquivo + label_dmsf_file_plural: DMS Arquivos # Email subject & Search options + label_dmsf_file_revision_plural: Revisões dos Arquivos + label_dmsf_file_revision_access_plural: Acessos aos Arquivos + warning_no_entries_selected: É necessário selecionar um arquivo ou pasta + error_email_to_must_be_entered: Destinatário não pode ficar em branco + warning_file_already_locked: Arquivo já bloqueado + notice_file_locked: Arquivo bloqueado + warning_file_not_locked: Arquivo não bloqueado + notice_file_unlocked: Arquivo desbloqueado + error_only_user_that_locked_file_can_unlock_it: Apenas o usuário que bloqueou o arquivo pode desbloqueá-lo + + error_max_files_exceeded: "Limite %{number} para arquivo baixados automaticamente" + error_entry_project_does_not_match_current_project: "Projeto de entrada não corresponde ao projeto atual" + + notice_folder_created: Pasta criada + error_folder_creation_failed: Erro na criação da pasta + error_folder_title_must_be_entered: Título deve ser digitado + notice_folder_deleted: Pasta deletada + error_folder_title_is_already_used: Título já utilizado + notice_folder_details_were_saved: Pasta atualizada + error_folder_is_locked: Pasta está bloqueada + error_file_is_locked: Arquivo está bloqueado + notice_file_deleted: Arquivo excluído + error_at_least_one_revision_must_be_present: Deve existir pelo menos uma revisão + notice_revision_deleted: Revisão excluída + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: Um dos arquivos bloqueados + notice_file_revision_created: Revisão do arquivo criado + notice_your_preferences_were_saved: Suas atualizações foram salvas + notice_your_preferences_were_not_saved: Suas preferências não foram salvas + warning_folder_notifications_already_activated: Notificações de pasta atualizadas + + notice_folder_notifications_activated: Notificações da pasta ativas + warning_folder_notifications_already_deactivateNd: Notificações da pasta desativadas + + notice_folder_notifications_deactivated: Notificações da pasta desativadas + warning_file_notifications_already_activated: Notificações de arquivo ativadas + + notice_file_notifications_activated: Notificações de arquivo ativadas + warning_file_notifications_already_deactivated: Notificações de arquivo já desativadas + + notice_file_notifications_deactivated: Notificações de arquivo desativadas + link_details: "%{title} detalhes" + link_edit: "Editar %{title}" + link_create_folder: Criar pasta + link_title: Taxonomia + link_size: Tamanho do arquivo + link_modified: Última modificação + link_ver: Revisão + link_author: Autor + title_check_for_zip_download_or_email: Marcar para download ou enviar por e-mail + title_check_for_restore_or_delete: Marcar para restaurar ou excluir + + title_notifications_active_deactivate: "Notificações ativas: Desativar" + title_notifications_not_active_activate: "Notificações não ativas: Ativar" + title_title_version_version_download: "%{title} revisão %{version} download" + title_locked_by_user: "Bloqueado por %{user}" + title_waiting_for_approval: Aguardando aprovação + title_approved: Aprovado + title_unlock_file: Clique aqui para permitir alterações por outro usuário + title_lock_file: Clique aqui para impedir alterações por outro usuário + title_download_checked: Download em arquivo compactado + title_send_checked_by_email: Enviar por e-mail + link_user_preferences: Suas preferências de projeto DMS + heading_send_documents_by_email: Enviar documentos por e-mail + label_email_from: Remetente + label_email_to: Destinatário + label_email_cc: CC + label_email_subject: Assunto + label_email_documents: DMS + label_email_body: Descrição + label_email_send: Enviar + title_notifications_active: Notificações Ativas + label_upload: Envio + heading_new_folder: Nova Pasta + label_title: Taxonomia + label_description: Descrição + submit_save: Salvar + info_file_locked: Arquivo bloqueado! + label_notifications: Notificações + select_option_default: Padrão + select_option_deactivated: Desativado + select_option_activated: Ativado + label_title_format: 'Formato do Título' + text_title_format: "Formato do título do documento para download (%t - título, %f - arquivo, %d - data, %v - versão, + %i - ID, %r - revisão). Exemplo: %t_%v" + title_save_preferences: Salvar preferências + heading_revisions: Revisões + title_download: Download + title_delete_revision: Revisão excluída + title_obsolete_revision: Obsolete revision + label_created: Criado + label_changed: Alterado + info_changed_by_user: "%{changed} por" + label_filename: Nome do arquivo + label_mime: Extensão do arquivo + label_size: Tamanho + heading_new_revision: Nova revisão + option_version_same: Mesma + option_version_patch: Patch + option_version_minor: Minor + option_version_major: Major + option_version_custom: Personalizado + label_new_content: Novo conteúdo + label_maximum_files_download: Máximo de download de arquivos + note_maximum_number_of_files_downloaded: Limite máximo de arquivos para download ou envio por e-mail de uma única vez. + + label_file_storage_directory: Diretório de armazenamento de arquivos + label_index_database: Index database + label_stemming_language: Stemming language + note_possible_values: Valores possíveis + note_pass_none_to_disable_stemming: "Passe 'none' para desabilitar derivação" + label_stem_strategy: Stem strategy + option_stem_none: Stem none (default) + option_stem_some: Stem some + option_stem_all: Stem all + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: Notificações padrões de arquivo + heading_uploaded_files: Arquivos enviados + link_documents: DMS + permission_view_dmsf_file_revision_accesses: Ver downloads no fluxo de Atividades + permission_view_dmsf_file_revisions: Ver revisões no fluxo de Atividades + permission_view_dmsf_folders: Browse documentos + permission_user_preferences: Preferências do usuário + permission_view_dmsf_files: Ver documentos + permission_folder_manipulation: Manipulação de pastas + permission_file_manipulation: Manipulação de arquivos + permission_force_file_unlock: Forçar desbloqueio do arquivo + permission_manage_workflows: Administrar workflows + permission_file_delete: Excluir arquivos + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: Arquivo + field_folder: Pasta + error_file_commit_require_uploaded_file: É necessário carregar um arquivo + + warning_some_files_were_not_committed: "O(s) arquivo(s) não foram salvos: %{files}" + + error_user_has_not_right_delete_folder: "Usuário não possui permissão para excluir pastas" + + error_user_has_not_right_delete_file: "Usuário não possui permissão para excluir arquivos" + + notice_entries_deleted: Excluído com sucesso + warning_some_entries_were_not_deleted: "Não foi possível excluir os seguintes arquivos: %{entries}" + title_delete_checked: Excluir documento ou arquivo selecionado + title_number_of_files_in_directory: Número de arquivos no diretório + title_filename_for_download: Nome do arquivo utilizado para for download ou arquivo compactado + label_number_of_folders: Pastas + label_number_of_documents: Documentos + error_file_storage_directory_does_not_exist: "O diretório de armazenamento de arquivo não existe e não pode ser + criado" + error_file_can_not_be_created: "O arquivo não pode ser criado" + error_wrong_zip_encoding: Código Zip errado + warning_xapian_not_available: Xapian não disponível + menu_dmsf: DMS + label_physical_file_delete: Excluir fisicamente arquivo + user_is_not_project_member: Você não é um membro do projeto + heading_access_downloads_emails: Downloads/Emails + heading_access_first: Primeiro + heading_access_last: Último + label_dmsf_updated: Updated + label_dmsf_downloaded: Downloaded + title_total_size_of_all_files: Tamanho total de todos os arquivos nesta pasta + project_module_dmsf: DMS + warning_no_project_to_copy_file_to: Nenhum projeto para cópia do arquivo + comment_copied_from: "Copiado de %{source}" + field_target_project: Projeto de destino + field_target_folder: Pasta de destino + title_copy_or_move: Copiar / Mover + label_dmsf_folder_plural: DMS Pastas # Search options + comment_moved_from: "Mover de %{source}" + error_target_folder_same: A pasta e o projeto alvo são os mesmos que os atuais + title_copy: Copiar + + error_max_email_filesize_exceeded: "Você excedeu o tamanho máximo do arquivo para enviar via e-mail. (%{number} MB)" + + note_maximum_email_filesize: Limita o tamanho máximo de arquivos que pode ser enviado por e-mail. 0 significa + ilimitado. O número está em MB. + label_maximum_email_filesize: Tamanho máximo do anexo de e-mail + header_minimum_filesize: Erro de arquivo. + error_minimum_filesize: "O arquivo %{file} é 0 bytes e não será anexado." + parent_directory: Diretório Pai + note_webdav: "Webdav uma vez habilitado pode ser encontrado em %{protocol}://%{domain}/dmsf/webdav/[project + identifier]" + label_copy_dmsf: "Copiar documentos e pastas (%{files} documentos em %{folders} pastas)" + label_copy_only_dmsf_folders: "Copiar pastas (%{folders})" + + warning_folder_already_locked: Esta pasta já está bloqueada + notice_folder_locked: A pasta foi bloqueada com sucesso + warning_folder_not_locked: A pasta não pode ser bloqueada + notice_folder_unlocked: A pasta foi desbloqueada com sucesso + error_only_user_that_locked_folder_can_unlock_it: Você não está autorizado a desbloquear esta pasta + + title_unlock_folder: Clique aqui para desbloquear e permitir alterações por outros usuários + title_lock_folder: Clique aqui para impedir alterações por outros usuários + + select_option_webdav_readonly: Read-only + select_option_webdav_readwrite: Read/Write + label_webdav_strategy: Webdav strategy + + note_webdav_strategy: Permite que o administrador decida se o webdav é uma plataforma read-only ou read-write para + usuários finais. + + error_unable_delete_dmsf_workflow: Não foi possível excluir o workflow + error_empty_note: "No caso de reprovação o preenchimento do camo notas é obrigatório" + error_workflow_assign: Ocorreu um erro ao atribuir workflow + error_cannot_start_workflow: "Workflow não pode ser iniciado" + error_cannot_renumber_steps: "Os passos não podem ser renumerados" + label_dmsf_workflow_new: Novo workflow de aprovação + field_label_dmsf_workflow: Workflow de aprovação + field_label_dmsf_workflow_name: Nome do workflow de aprovação + label_dmsf_workflow_plural: Workflows de aprovação + label_dmsf_workflow_plural_num: Workflows de aprovação(%{count}) + label_dmsf_workflow_step: Passo + label_dmsf_workflow_step_plural: Passos + label_dmsf_workflow_approval_plural: Aprovações + label_dmsf_wokflow_action_approve: Aprovar + label_dmsf_wokflow_action_reject: Reprovar + label_dmsf_wokflow_action_delegate: Atribuir para + label_dmsf_wokflow_action_assign: Atribuir workflow de aprovação + label_dmsf_wokflow_action_start: Iniciar workflow + label_dmsf_workflow_add_approver: "Adicionar uma nova função lógica de aprovação:" + label_or: ou + label_action: Ação + label_note: Observação + title_none: Nenhum + title_rejection: Rejeição + title_delegation: Delegação + title_assignment: Atribuição + title_start: Iniciado + title_dmsf_workflow_log: Registro do workflow de aprovação + title_assigned: Atribuído + title_approval: Aprovado + title_rejected: Rejeitado + title_obsolete: Obsolete + dmsf_and: E + dmsf_or: OU + dmsf_new_step: Novo passo + dmsf_new_step_or_approver: Novo passo ou Novo aprovador + message_dmsf_wokflow_note: Sua observação... + info_revision: "r %{rev}" + link_workflow: Workflow + notice_workflow_started: Workflow de aprovação foi iniciado com êxito + text_email_subject_approved: aprovado + text_email_subject_rejected: reprovado + text_email_subject_delegated: atribuído para + text_email_subject_requires_approval: requer sua aprovação + text_email_subject_updated: atualizado + text_email_subject_started: iniciado + text_email_finished_approved: "O workflow de aprovação'%{name}' definido para o documento '%{filename}' foi finalizado + e o documento foi aprovado." + text_email_finished_rejected: "O workflow de aprovação '%{name}' definido para o documento '%{filename}' foi + finalizado e o documento foi reprovado devido a '%{notice}'." + text_email_finished_delegated: "O workflow de aprovação '%{name}' definido para o documento '%{filename}' foi + atribuido pois '%{notice}' e está aguardando a sua aprovação na etapa atual '%{stepname}'." + text_email_finished_step: "O workflow de aprovação '%{name}' definido para o documento '%{filename}' foi aprovado na + etapa anterior e está aguardando a sua aprovação na etapa atual." + text_email_finished_step_short: "O workflow de aprovação '%{name}' definido para o documento '%{filename}' foi + aprovado em uma das suas etapas de aprovação." + text_email_started: "O workflow de aprovação '%{name}' definido para o documento '%{filename}' foi iniciado e está + aguardando a sua aprovação na etapa atual '%{stepname}'." + text_email_to_proceed: Para dar continuidade ao fluxo, clique no íncone ao lado do documento + text_email_to_see_history: Para visualizar o histórico de workflow de aprovação clique no status do workflow + disponível ao lado do documento + text_email_to_see_status: Para visualizar o status atual do workflow de aprovação, clique no status de fluxo de + trabalho do documento + + title_create_link: Crie um link simbólico + label_link_from: Link de + label_link_to: Link para + label_notifications_on: Ativar Notificações + label_notifications_off: Desativar Notificações + field_target_file: Arquivo fonte + title_download_entries: Download entries + label_external: External + label_internal: Internal + + label_link_name: Link name + field_external_url: URL + label_target_folder: Target folder + label_source_folder: Source folder + label_target_project: Target project + label_source_project: Source project + + text_email_doc_updated_subject: Documentos atualizados + text_email_doc_updated: atualizou os documentos da área + text_email_doc_follows: as follows + text_email_doc_deleted_subject: Exclusão de documentos + text_email_doc_deleted: deletou os documentos da área + label_links_only: links apenas + + label_display_notified_recipients: Mostrar destinatários notificados + note_display_notified_recipients: O usuário será informado sobre todos os destinatários que enviou a notificação por + e-mail. + warning_email_notifications: "Notificações de e-mail enviadas para %{to}" + + link_trash_bin: Lixeira + title_restore: Restaurar + notice_dmsf_file_restored: O documento foi restaurado com sucesso + notice_dmsf_folder_restored: A pasta foi restaurada com sucesso + notice_dmsf_link_restored: O link foi restaurado com sucesso + title_restore_checked: Restaurar pasta ou arquivo + error_parent_folder: "A pasta de pai não existe" + + error_resource_or_parent_locked: Não é possível completar o bloqueio - recurso (ou pai) está bloqueado + error_parent_locked: Não é possível completar o bloqueio - o recurso pai está bloqueado + error_resource_locked: Não foi possível concluir o bloqueio - o recurso está bloqueado + error_lock_exclusively: Não é possível bloquear exclusivamente um recurso já bloqueado + error_unlock_parent_locked: Falha no desbloqueio - o recurso pai está bloqueado + + label_dmsf_version: Versão + + locked_documents: Documentos bloqueados + open_approvals: Aprovações abertas + watched_documents: Watched documents + + error_maximum_upload_filecount: "Não mais que %{filecount} arquivo(s) pode(m) ser carregado(s)." + + label_public_urls: URLs públicas válidas para + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Modelo de arquivos ignorados + note_webdav_ignore: Uma expressão regular com nomes de arquivos para serem ignorados por requisições PUT. + + label_document_url: Url + label_last_revision_id: Revisão + + label_webdav_disable_versioning: Não há modelo de arquivos de versão + note_webdav_disable_versioning: Uma expressão regular que desabilita o controle de versão para os arquivos + correspondentes. O modelo padrão corresponde aos arquivos temporários criados pelo MsOffice. + + label_dmsf_keep_documents_locked: Manter documentos bloqueados + note_dmsf_keep_documents_locked: Documentos serão mantidos bloqueados quando aprovados + note_global: (global) + field_dmsf_not_inheritable: Não herdável + + label_webdav_use_project_names: Use o nome do projeto para a pasta do projeto + note_webdav_use_project_names: Use nomes de projetos em vez de identificador de projeto para pastas de projeto. + + label_last_approver: Last approver + + label_act_as_attachable: Atuar como anexo + note_dmsf_act_as_attachable: Permite anexar documentos a objetos, por exemplo, issues. + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: Fluxo + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: Tamanho + field_locked: Bloqueado + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: Título + label_column_size: Tamanho + label_column_modified: Alterada + label_column_version: Versão + label_column_workflow: Fluxo + label_column_author: Autor + label_column_description: Descrição + label_column_comment: Comentário + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: Contém caracteres inválidos \ No newline at end of file diff --git a/config/locales/sl.yml b/config/locales/sl.yml new file mode 100644 index 00000000..f4b76e58 --- /dev/null +++ b/config/locales/sl.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Zdravko Balorda , Karel Pičman +# +# 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 +# . + +sl: + dmsf: Arhiv # Custom fields tab title + label_dmsf_file: DMS Datoteka + label_dmsf_file_plural: DMS Arhivske datoteke # Email subject & Search options + label_dmsf_file_revision_plural: Document revisions + label_dmsf_file_revision_access_plural: Document accesses + warning_no_entries_selected: Ničesar niste izbrali + error_email_to_must_be_entered: Email Naslovnik mora bit izbran + warning_file_already_locked: Datoteka že zaklenjena + notice_file_locked: Datoteka zaklenjena + warning_file_not_locked: Datoteka ni zaklenjena + notice_file_unlocked: Datoteka odklenjena + error_only_user_that_locked_file_can_unlock_it: Samo oseba, ki je zaklenila datoteko, jo lahko odklene. + + error_max_files_exceeded: "Max %{number} datotek za istočasno nalaganje je preseženo." + error_entry_project_does_not_match_current_project: Projekt se ujema s trenutno nastavljenim projektom + + notice_folder_created: Mapa kreirana + error_folder_creation_failed: Mape ne morem kreirati + error_folder_title_must_be_entered: Naslov mora biti vnešen + notice_folder_deleted: Mapa izbrisana + error_folder_title_is_already_used: Naslov je že uporabljen + notice_folder_details_were_saved: Podatki o mapi so shranjeni + error_folder_is_locked: Папка заблокированa + error_file_is_locked: Mapa je zaklenjena + notice_file_deleted: Datoteka izbrisana + error_at_least_one_revision_must_be_present: Vsaj ena verzija mora obstajati + notice_revision_deleted: Verzija izbrisana + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: Ena od datotek je zaklenjena + notice_file_revision_created: Verzija datoteke kreirana + notice_your_preferences_were_saved: Vaše nastavitve so shranjene + notice_your_preferences_were_not_saved: Your preferences were not saved + warning_folder_notifications_already_activated: Obveščanje o mapi je že aktivno + + notice_folder_notifications_activated: Obveščanje o mapi je aktivirano + warning_folder_notifications_already_deactivated: Obveščanje o mapi je že neaktivno + + notice_folder_notifications_deactivated: Obveščanje o mapi je deaktivirano + warning_file_notifications_already_activated: Obveščanje o datoteki je že aktivno + + notice_file_notifications_activated: Obveščanje o datoteki je aktivirano + warning_file_notifications_already_deactivated: Obveščanje o datoteki je že deaktivirano + + notice_file_notifications_deactivated: Obveščanje o datoteki je deaktivirano + link_details: "%{title} podrobnosti" + link_edit: "Uredi %{title}" + link_create_folder: Kreiraj mapo + link_title: Naslov + link_size: Velikost + link_modified: Spremenjeno + link_ver: Ver. + link_author: Autor + title_check_for_zip_download_or_email: Označi za zip prenašanje ali email + title_check_for_restore_or_delete: Check for restore or delete + + title_notifications_active_deactivate: "Obveščanje aktivno: Deaktiviraj" + title_notifications_not_active_activate: "Obveščanje ni aktivno: Aktiviraj" + title_title_version_version_download: "%{title} verzija %{version} prenesi dol" + title_locked_by_user: "Zaklenil %{user}" + title_waiting_for_approval: V postopku odobritve + title_approved: Odobreno + title_unlock_file: Odkleni drugim članom za posodabljanje + title_lock_file: Zakleni za posodabljanje + title_download_checked: Prenesi izbrano v Zip formatu + title_send_checked_by_email: Pošlji izbrano po elektronski pošti + link_user_preferences: Vaše Arhivske nastavitve za projekt + heading_send_documents_by_email: Pošlji dokumente po elektronski pošti + label_email_from: Pošiljatelj + label_email_to: Naslovnik + label_email_cc: Kp + label_email_subject: Zadeva + label_email_documents: Priloge + label_email_body: Vsebina + label_email_send: Pošlji + title_notifications_active: Obveščanje aktivno + label_upload: Naloži + heading_new_folder: Nova mapa + label_title: Naziv mape + label_description: Opis + submit_save: Shrani + info_file_locked: Datoteka zaklenjena! + label_notifications: Obveščanje + select_option_default: Privzeto + select_option_deactivated: Deaktivirano + select_option_activated: Aktivirano + label_title_format: Title format + text_title_format: "Document title format for download (%t - title, %f - file, %d - date, %v - version, %i - ID, %r - + revision). Example: %t_%v" + title_save_preferences: Save preferences + heading_revisions: Verzije + title_download: Prenesi dol + title_delete_revision: Izbriši verzijo + title_obsolete_revision: Obsolete revision + label_created: Narejeno + label_changed: Spremenjeno + info_changed_by_user: "%{changed} po" + label_filename: Datoteka + label_mime: Mime + label_size: Velikost + heading_new_revision: Nova verzija + option_version_same: Enako + option_version_patch: Patch + option_version_minor: Minor + option_version_major: Major + option_version_custom: Custom + label_new_content: Nova vsebina + label_maximum_files_download: Maximum datotek za prenos dol. + note_maximum_number_of_files_downloaded: Omejitev največjega števila prenešenih datotek v zip formatu ali poslanih po + emailu. 0 pomeni neomejeno. + label_file_storage_directory: Direktorij za datoteke + label_index_database: Index podatkovna baza + label_stemming_language: Jezik za korenjenje besed (stemming) + note_possible_values: Možne vrednosti + note_pass_none_to_disable_stemming: "vnesi 'none' za izklop korenjenja besed (stemming)" + label_stem_strategy: Strategija korenjenja besed + option_stem_none: Izklop korenjenja (privzeto) + option_stem_some: Korenjenje nekaterih besed + option_stem_all: Korenjenje vseh besed + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: Privzeti status obveščanja za datoteke + heading_uploaded_files: Naložene datoteke + link_documents: Dokumentacija + permission_view_dmsf_file_revision_accesses: View downloads in Activity stream + permission_view_dmsf_file_revisions: View revisions in Activity stream + permission_view_dmsf_folders: Brskanje po dokumentih + permission_user_preferences: Uporabniške nastavitve + permission_view_dmsf_files: Preglej dokumente + permission_folder_manipulation: Upravljanje z mapami + permission_file_manipulation: Upravljanje z datotekami + permission_force_file_unlock: Prisilno odkleni datoteko + permission_manage_workflows: Manage workflows + permission_file_delete: Delete documents + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: Datoteka + field_folder: Pod mapo + error_file_commit_require_uploaded_file: Ukaz zahteva naloženo datoteko + + warning_some_files_were_not_committed: "Nekatere datoteke niso shranje zaradi napak v validaciji: %{files}" + + error_user_has_not_right_delete_folder: Uporabnik nima privilegija brisati mapo + + error_user_has_not_right_delete_file: Uporabnik nima privilegija brisati datoteke + + notice_entries_deleted: Izbrane enote izbrisane + warning_some_entries_were_not_deleted: "Nekatere enote niso izbrisane: %{entries}" + title_delete_checked: Izbriši izbrano + title_items: items + title_filename_for_download: Naziv datoteke za prenos dol ali Zip arhiva + label_number_of_folders: Mapa + label_number_of_documents: Dokumentacija + error_file_storage_directory_does_not_exist: Mapa za datoteke ne obstaja in se je ne da kreirat + + error_file_can_not_be_created: Datoteke se ne da kreirat v mapi + error_wrong_zip_encoding: Napačen Zip nabor znakov + warning_xapian_not_available: Xapian ni na voljo + menu_dmsf: Arhiv # Project tab title + label_physical_file_delete: Brisanje datoteke (fizično) + user_is_not_project_member: Niste član projekta + heading_access_downloads_emails: "Prenosi/Email-i" + heading_access_first: Prvi + heading_access_last: Zadnji + label_dmsf_updated: Arhiv posodobljen + label_dmsf_downloaded: Arhiv downloaded + title_total_size_of_all_files: Skupna velikost vseh datotek v tej mapi + project_module_dmsf: Arhiv # Project module name + warning_no_project_to_copy_file_to: Ni projekta kamor bi kopiral datoteko + comment_copied_from: "Skopirano iz %{source}" + field_target_project: Ciljni projekt + field_target_folder: Ciljna mapa + title_copy_or_move: Kopiraj/Premakni + label_dmsf_folder_plural: DMS Arhivske mape # Search options + comment_moved_from: "Premaknjeno iz %{source}" + error_target_folder_same: Ciljna mapa in projekt sta ista kot izvorna (trenutna) + title_copy: Kopiraj + + error_max_email_filesize_exceeded: "Presegli ste največjo velikost datoteke za pošiljanje po email-u. (%{number} MB)" + + note_maximum_email_filesize: Omejitev največje velikosti datoteke, ki se lahko pošlje po email-u. 0 pomeni neomejeno. + Količina je v MB. + label_maximum_email_filesize: Največja velikost email priponke + header_minimum_filesize: Datotečna napaka. + error_minimum_filesize: "Datoteka %{file} je 0 bytov in ne bo pripeta." + parent_directory: Nadrejena mapa + note_webdav: "Webdav po vklopu lahko najdete na %{protocol}://%{domain}/dmsf/webdav/[project identifier]" + + label_copy_dmsf: "Kopiraj Arhivske datoteke in mape (%{files} datoteke v %{folders} mape)" + label_copy_only_dmsf_folders: "Kopiraj mape (%{folders})" + + warning_folder_already_locked: Mapa je že zaklenjena + notice_folder_locked: Mapa je uspešno zaklenjena + warning_folder_not_locked: Ne, ta mapa ne more biti zaklenjena + notice_folder_unlocked: Mapa je uspešno odklenjena + error_only_user_that_locked_folder_can_unlock_it: Nimate privilegijev, da bi odklenili to mapo + title_unlock_folder: Odkleni, da bi drugim članom omogočil spreminjanje + + title_lock_folder: Zakleni, da bi drugim članom preprečil spreminjanje + + select_option_webdav_readonly: Beri (izključno) + select_option_webdav_readwrite: "Beri/Piši" + label_webdav_strategy: Webdav strategija + + note_webdav_strategy: "Omogoči administratorju da odloči ali je webdav platforma na voljo izključno za branje ali + beri/piši za končne uporabnike." + + error_unable_delete_dmsf_workflow: Unable to delete the workflow + error_empty_note: "The note can't be empty" + error_workflow_assign: An error occured while assigning + error_cannot_start_workflow: "Workflow can't be started" + error_cannot_renumber_steps: "Steps can't be renumbered" + label_dmsf_workflow_new: New approval workflow + field_label_dmsf_workflow: Approval Workflow + field_label_dmsf_workflow_name: Approval workflow name + label_dmsf_workflow_plural: Approval workflows + label_dmsf_workflow_plural_num: Copy approval workflows (%{count}) + label_dmsf_workflow_step: Step + label_dmsf_workflow_step_plural: Steps + label_dmsf_workflow_approval_plural: Approvals + label_dmsf_wokflow_action_approve: Approve + label_dmsf_wokflow_action_reject: Reject + label_dmsf_wokflow_action_delegate: Delegate to + label_dmsf_wokflow_action_assign: Assign an approval workflow + label_dmsf_wokflow_action_start: Start workflow + label_dmsf_workflow_add_approver: "Add a new approver with a logical function:" + label_or: or + label_action: Action + label_note: Note + title_none: None + title_rejection: Rejection + title_delegation: Delegation + title_assignment: Assignment + title_start: Start + title_dmsf_workflow_log: Approval Workflow Log + title_assigned: Assigned + title_approval: Approval + title_rejected: Rejected + title_obsolete: Obsolete + dmsf_and: AND + dmsf_or: OR + dmsf_new_step: New step + dmsf_new_step_or_approver: New step or New approver + message_dmsf_wokflow_note: Your note... + info_revision: "r %{rev}" + link_workflow: Workflow + notice_workflow_started: Approval workflow successfully started + text_email_subject_approved: approved + text_email_subject_rejected: rejected + text_email_subject_delegated: delegated + text_email_subject_requires_approval: requires your approval + text_email_subject_updated: updated + text_email_subject_started: started + text_email_finished_approved: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + finished and the document has been approved." + text_email_finished_rejected: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + finished and the document has been rejected because of '%{notice}'." + text_email_finished_delegated: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + delegated because of '%{notice}' and you are expected to do an approval in the current approval step '%{stepname}'." + text_email_finished_step: "The approval workflow '%{name}' assigned to '%{filename}' document has just finished one of + the approval steps and you are expected to do an approval in the next approval step." + text_email_finished_step_short: "The approval workflow '%{name}' assigned to '%{filename}' document has just finished + one of the approval steps." + text_email_started: "The approval workflow '%{name}' assigned to '%{filename}' document has just been started and you + are expected to do an approval in the current approval step '%{stepname}'." + text_email_to_proceed: To proceed click on the check box icon next to the document in + text_email_to_see_history: To see the approval history click on the workflow status of the document in + + text_email_to_see_status: To see the current status of the approval workflow click on the workflow status the document + in + + title_create_link: Create a symbolic link + label_link_from: Link from + label_link_to: Link to + label_notifications_on: Notifications on + label_notifications_off: Notifications off + field_target_file: Source file + title_download_entries: Download entries + label_external: External + label_internal: Internal + + label_link_name: Link name + field_external_url: URL + label_target_folder: Target folder + label_source_folder: Source folder + label_target_project: Target project + label_source_project: Source project + + text_email_doc_updated_subject: Documents updated + text_email_doc_updated: has just actualized documents of + text_email_doc_follows: as follows + text_email_doc_deleted_subject: Documents deleted + text_email_doc_deleted: has just deleted documents of + label_links_only: links only + + label_display_notified_recipients: Display notified recipients + note_display_notified_recipients: The user will be informed about all recipients of just sent the email notification. + + warning_email_notifications: "Email notifications sent to %{to}" + + link_trash_bin: Trash bin + title_restore: Restore + notice_dmsf_file_restored: The document has been successfully restored + notice_dmsf_folder_restored: The folder has been successfully restored + notice_dmsf_link_restored: The link has been successfully restored + title_restore_checked: Restore checked + error_parent_folder: "The parent folder doesn't exist" + + error_resource_or_parent_locked: Unable to complete lock - resource (or parent) is locked + error_parent_locked: Unable to complete lock - resource parent is locked + error_resource_locked: Unable to complete lock - resource is locked + error_lock_exclusively: Unable to lock exclusively an already-locked resource + error_unlock_parent_locked: Unlock failed - resource parent is locked + + label_dmsf_version: Verzija + + locked_documents: Locked documents + open_approvals: Open approvals + watched_documents: Watched documents + + error_maximum_upload_filecount: "No more than %{filecount} file(s) can be uploaded." + + label_public_urls: Public URLs valid to + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Ignored files patterns + note_webdav_ignore: A regular expresion with filenames to ignore by PUT requests. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. The default pattern + matches temporary files created by MsOffice. + + label_dmsf_keep_documents_locked: Keep documents locked + note_dmsf_keep_documents_locked: Documents will be kept locked when approved + note_global: (global) + field_dmsf_not_inheritable: Not inheritable + + label_webdav_use_project_names: Use project name for project folder + note_webdav_use_project_names: Use project names instead of project identifier for project folders. + + label_last_approver: Last approver + + label_act_as_attachable: Act as attachable + note_dmsf_act_as_attachable: Allows to attach documents to objects e.g. issues. + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: Potek dela + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: Velikost + field_locked: Zaklenjena + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: Naslov + label_column_size: Velikost + label_column_modified: Spremenjeno + label_column_version: Verzija + label_column_workflow: Potek dela + label_column_author: Avtor + label_column_description: Opis + label_column_comment: Komentar + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: vsebuje nedovoljene znake \ No newline at end of file diff --git a/config/locales/uk.yml b/config/locales/uk.yml new file mode 100644 index 00000000..dec75710 --- /dev/null +++ b/config/locales/uk.yml @@ -0,0 +1,500 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +uk: + dmsf: ЕДО # Custom fields tab title - Заголовок таблиці спеціальних полів + label_dmsf_file: ЕДО Документ + label_dmsf_file_plural: ЕДО Документи # Email subject & Search options - Документи DMS # Тема електронного листа і опції пошуку + label_dmsf_file_revision_plural: Перегляд / Редакція документа + label_dmsf_file_revision_access_plural: Доступ до документів + warning_no_entries_selected: Запис не вибрано + error_email_to_must_be_entered: Потрібно ввести електронну адресу + warning_file_already_locked: Файл вже заблокований + notice_file_locked: Файл заблоковано + warning_file_not_locked: Файл не заблоковано + notice_file_unlocked: Файл розблоковано + error_only_user_that_locked_file_can_unlock_it: Лише користувач, який заблокував файл, може його розблокувати + + error_max_files_exceeded: "Перевищено ліміт файлів- %{number}, які можна завантажувати одночасно" + error_entry_project_does_not_match_current_project: "Введений проект не відповідає поточному проекту" + + notice_folder_created: Створена папка + error_folder_creation_failed: Не вдалося створити папку + error_folder_title_must_be_entered: Необхідно ввести назву + notice_folder_deleted: Папку видалено + error_folder_title_is_already_used: Назва вже використана + notice_folder_details_were_saved: Деталі папки збережено + error_folder_is_locked: Папка заблокована + error_file_is_locked: Файл заблокований + notice_file_deleted: Файл видалений + error_at_least_one_revision_must_be_present: Повинна бути принаймні одна редакція + notice_revision_deleted: Зміни видалено + notice_revision_obsoleted: Зміни застарли + warning_one_of_files_locked: Один із файлів заблоковано + notice_file_revision_created: Створена нова редакція файла + notice_your_preferences_were_saved: Ваші налаштування були збережені + notice_your_preferences_were_not_saved: Ваші налаштування не були збережені + warning_folder_notifications_already_activated: Сповіщення про папку вже активовано + + notice_folder_notifications_activated: Сповіщення про папку активовано + warning_folder_notifications_already_deactivated: Сповіщення про папку вже деактивовано + + notice_folder_notifications_deactivated: Сповіщення про папку деактивовано + warning_file_notifications_already_activated: Сповіщення про файл вже активовано + + notice_file_notifications_activated: Сповіщення про файл активовано + warning_file_notifications_already_deactivated: Сповіщення про файл вже деактивовано + + notice_file_notifications_deactivated: Сповіщення про файл деактивовано + link_details: "Деталі %{title}" + link_edit: "Редагувати %{title}" + link_create_folder: Створити папку + link_title: Назва + link_size: Розмір + link_modified: Змінено + link_ver: Версія + link_author: Автор + title_check_for_zip_download_or_email: Перевірте архів, завантаження чи електронну адресу + title_check_for_restore_or_delete: Перевірте відновлення або видалення + + title_notifications_active_deactivate: "Сповіщення активні: Деактивувати" + title_notifications_not_active_activate: "Сповіщення не активні: Активувати" + title_title_version_version_download: "Назва версії %{title} завантаження %{version}" + title_locked_by_user: "Заблоковано користувачем -%{user}" + title_waiting_for_approval: Очікує підтвердження + title_approved: Підтверджено + title_unlock_file: Розблокуйте, щоб дозволити зміни іншим учасникам + title_lock_file: Заблокуйте, щоб запобігти змінам від інших учасників + title_download_checked: Завантаження перевірено в Zip-архіві + title_send_checked_by_email: Відправити перевірене електронною поштою + link_user_preferences: Ваші налаштування проекту DMS + heading_send_documents_by_email: Надіслати документ електронною поштою + label_email_from: Від + label_email_to: Кому + label_email_cc: Копія + label_email_subject: Тема + label_email_documents: Документи + label_email_body: Тіло + label_email_send: Відправити + title_notifications_active: Сповіщення активні + label_upload: Вивантажити + heading_new_folder: Нова папка + label_title: Назва + label_description: Опис + submit_save: Зберегти + info_file_locked: Файл заблоковано! + label_notifications: Сповіщення + select_option_default: За замовчуванням + select_option_deactivated: Деактивовано + select_option_activated: Активовано + label_title_format: Форамт назви + text_title_format: "Формат назви документа для завантаження (%t - title, %f - file, %d - date, %v - version, %i - ID, + %r - revision)(назва-файл-дата-версія-номер-редакція). Example: %t_%v" + title_save_preferences: Зберегти зміни + heading_revisions: Зміни + title_download: Завантажити + title_delete_revision: Видалити зміни + title_obsolete_revision: Зміни застаріли + label_created: Створено + label_changed: Змінено + info_changed_by_user: "Змінено %{changed} by" + label_filename: "Ім'я файлу" + label_mime: Mime - Іконка + label_size: Size - Розмір + heading_new_revision: Нова редакція + option_version_same: Те саме + option_version_patch: Шлях + option_version_minor: Незначні + option_version_major: Глобальні + option_version_custom: Налаштовані + label_new_content: Новий зміст + label_maximum_files_download: Завантажена максимальна кількість файлів + note_maximum_number_of_files_downloaded: Обмежує максимальну кількість файлів, завантажених у zip або надісланих + електронною поштою. 0 означає необмежений. + label_file_storage_directory: Директорія зберігання файлів + label_index_database: Індекс бази даних + label_stemming_language: Основна мова + note_possible_values: Можливі значення + note_pass_none_to_disable_stemming: "натисніть 'none'(ні), щоб вимкнути основну індикацію" + label_stem_strategy: Коренева стратегія + option_stem_none: Без стематизації (за замовченням) + option_stem_some: Часткова стематизація + option_stem_all: Повна стематизація + text_stemming_info: "Це налаштування визначає, як аналізатор запитів застосовуватиме алгоритм пошуку кореневої форми + (стематизації). Стандартне значення – Без стематизації. Допустимі значення: Без стематизації - Не застосовувати + алгоритм пошуку кореневої форми, Часткова стематизація - Пошук по кореням термінів, окрім тих, що починаються з + великої літери, або супроводжуються певними символами (наразі:'/@<>=*[{\"'), або використовуються з операторами, + яким потрібна інформація про місцезнаходження. Стематизовані терміни мають префікс 'Z', Повна стематизація - Шукає + кореневу форму всіх слів (примітка: префікс 'Z' не додається)." + label_default_notifications: Файл cповіщення за умовчанням + heading_uploaded_files: Вивантажені файли + link_documents: Документи + permission_view_dmsf_file_revision_accesses: Перегляд завантажень у Потоці активності + permission_view_dmsf_file_revisions: Перегляд змін у Потоці активності + permission_view_dmsf_folders: Перегляньте документи + permission_user_preferences: Зміни користувача + permission_view_dmsf_files: Переглянути документи + permission_folder_manipulation: Маніпуляції з папками + permission_file_manipulation: Маніпуляції з файлами + permission_force_file_unlock: Примусове розблокування файла + permission_manage_workflows: Керуванн робочими процесами + permission_file_delete: Видалити документи + permission_display_system_folders: Відобразити системні папки + permission_file_approval: Підтвердження файлу + permission_email_documents: Документи з електронної пошти + label_file: Файл + field_folder: Папка + error_file_commit_require_uploaded_file: Для збереження файлів потрібен завантажений файл + + warning_some_files_were_not_commited: "Деякі файли не було зафіксовано-збережено через помилки перевірки : %{files}" + + error_user_has_not_right_delete_folder: "Користувач не має права видаляти папки" + + error_user_has_not_right_delete_file: "Коричтувач не має права видаляти файл" + + notice_entries_deleted: Записи видалено + warning_some_entries_were_not_deleted: "Деякі записи було видалено : %{entries}" + title_delete_checked: Видалити перевірені + title_items: елемени + title_filename_for_download: Ім'я файла використано для завантаження або в Zip-архіві + label_number_of_folders: Folders - Папки + label_number_of_documents: Documents - Документи + error_file_storage_directory_does_not_exist: "Каталог/Директорія зберігання файлів не існує і не може бути створений" + + error_file_can_not_be_created: "Неможливо створити файли в каталозі/директорії зберігання" + error_wrong_zip_encoding: Неправильне Zip-кодування + warning_xapian_not_available: Xapian not available + menu_dmsf: DMS # Project tab title - Заголовок вкладки DMS # Project + label_physical_file_delete: Фізичний файл видалено + user_is_not_project_member: Ви не є учасником проекту + heading_access_downloads_emails: Завантажити/Емейл + heading_access_first: Перший + heading_access_last: Останній + label_dmsf_updated: Оновлений + label_dmsf_downloaded: Завантажений + title_total_size_of_all_files: Загальний розмір файлів у цій папці + project_module_dmsf: DMS # Project module name - Ім'я/Назва модуля DMS # Project + warning_no_project_to_copy_file_to: Немає проекту, куди можна скопіювати файл + comment_copied_from: "Скопійовано з %{source}" + field_target_project: Цільовий/Кінцевий проект + field_target_folder: Цільова/Кінцева папка + title_copy_or_move: Скопіювати/Перенести + label_dmsf_folder_plural: DMS Folders # Search options - Опцї пошуку папок + comment_moved_from: "Переміщено з %{source}" + error_target_folder_same: Цільова/кінцева папка і проект такі самі, як поточні + title_copy: Копіювати + + error_max_email_filesize_exceeded: "Ви перевищили максимальний розмір файлу для надсилання електронною поштою (%{number} MB)" + + note_maximum_email_filesize: Обмежує максимальний розмір файлу, який можна надіслати електронною поштою. 0 означає необмежений. Цифра в МБ + + label_maximum_email_filesize: Максимальний розмір вкладення електронної пошти + header_minimum_filesize: Помилка файла + error_minimum_filesize: "Файл %{file} розміром 0 байт і не може бути прикріплений" + parent_directory: Батьківський каталог + note_webdav: "Після ввімкнення протокол Webdav можна знайти за адресою %{protocol}://%{domain}/dmsf/webdav/[project identifier]" + + label_copy_dmsf: "Скопіювати документи і папки до (%{files} files in %{folders} folders)" + label_copy_only_dmsf_folders: "Скопіювати лише папки (%{folders})" + + warning_folder_already_locked: Ця папка вже заблокована + notice_folder_locked: Папку успішно заблоковано + warning_folder_not_locked: На жаль, не вжалося розблокувати папку + notice_folder_unlocked: Папку успішно розблоковано + error_only_user_that_locked_folder_can_unlock_it: У вас немає прав для розблокування цієї папки + + title_unlock_folder: Розблокуйте, щоб дозволити зміни від інших учасників + title_lock_folder: Заблокуйте, щоб запобігти змінам від інших учасників + + select_option_webdav_readonly: Лише для читання + select_option_webdav_readwrite: Читання/Редагування + label_webdav_strategy: Стратегія Webdav + + note_webdav_strategy: Дозволяє адміністратору вирішувати, чи платформа Webdav є доступною лише для читання, чи читання/редагування для кінцевих користувачів. + + + error_unable_delete_dmsf_workflow: Не вдалося видалити робочий процес + error_empty_note: "Примітка не може бути порожньою" + error_workflow_assign: Під час призначення сталася помилка + error_cannot_start_workflow: "Не вдалося почати робочий процес" + error_cannot_renumber_steps: "Не можна змінити нумерацію кроків" + label_dmsf_workflow_new: Новий робочий процес затвердження + field_label_dmsf_workflow: Робочий процес затвердження + field_label_dmsf_workflow_name: Назва процесу затвердження + label_dmsf_workflow_plural: Процеси затвердження + label_dmsf_workflow_plural_num: Копіювати процеси затвердження (%{count}) + label_dmsf_workflow_step: Step - Крок + label_dmsf_workflow_step_plural: Кроки + label_dmsf_workflow_approval_plural: Затвердження + label_dmsf_wokflow_action_approve: Підтвердити + label_dmsf_wokflow_action_reject: Відхилити + label_dmsf_wokflow_action_delegate: Делегувати + label_dmsf_wokflow_action_assign: Призначити процес затвердження + label_dmsf_wokflow_action_start: Почати робочий процес + label_dmsf_workflow_add_approver: "Додайте нового затверджувача з логічною функцією" + label_or: or - або + label_action: Дія + label_note: Примітка + title_none: Жодної + title_rejection: Відміна + title_delegation: Делегування + title_assignment: Призначення + title_start: Почати + title_dmsf_workflow_log: Журнал робочого процесу затвердження + title_assigned: Призначено + title_approval: Підтвердження + title_rejected: Відхилено + title_obsolete: Застріло + dmsf_and: AND - І + dmsf_or: OR - АБО + dmsf_new_step: Новий крок + dmsf_new_step_or_approver: Новий крок або Новий затверджувач + message_dmsf_wokflow_note: Ваша примітка... + info_revision: "r %{rev}" + link_workflow: Робочий процес + notice_workflow_started: Підтвердження робочого процесу почалося усішно + text_email_subject_approved: підтверджено + text_email_subject_rejected: відхилено + text_email_subject_delegated: делеговано + text_email_subject_requires_approval: потрібне ваше підтвердження + text_email_subject_updated: оновлено + text_email_subject_started: почато + text_email_finished_approved: "Процес підтвердження '%{name}' для документа '%{filename}' щойно закінчився і документ + схвалено." + text_email_finished_rejected: "Процес підтвердження '%{name}' для документа призначений '%{filename}' щойно закінчився + і документ відхилено через '%{notice}'." + text_email_finished_delegated: "Процес підтвердження '%{name}' для документа '%{filename}' був делегований через + '%{notice}' і ви повинні підтвердити його на відповідному кроці '%{stepname}'" + text_email_finished_step: "Робочий процес '%{name}' призначений для документу '%{filename}' затвердження щойно + закінчив один із кроків підтвердження і ви повинні зробити підтвердження на наступному кроці." + text_email_finished_step_short: "Робочий процес затвердження '%{name}', призначений документу '%{filename}', щойно + завершив один із етапів затвердження." + text_email_started: "Робочий процес затвердження '%{name}', призначений документу '%{filename}', щойно розпочато, і ви + очікується, що буде виконано схвалення на поточному кроці схвалення '%{stepname}'." + text_email_to_proceed: Щоб продовжити, клацніть на іконку прапорця поруч із документом + text_email_to_see_history: Щоб переглянути історію підтвердження, клацніть на статус робочого процесу документа + + text_email_to_see_status: Щоб переглянути поточний статус робочого процесу підтвердження, клацніть на статус робочого + процесу документа у + + title_create_link: Створити символічний лінк + label_link_from: Лінк від + label_link_to: Лінк до + label_notifications_on: Увімкнути сповіщення + label_notifications_off: Вимкнути сповіщення + field_target_file: Вихідний файл + title_download_entries: Завантажити зміни + label_external: Зовнішні + label_internal: Внутрішні + + label_link_name: Назва лінку + field_external_url: URL + label_target_folder: Кінцева папка + label_source_folder: Вихідна папка + label_target_project: Кінцевий проект + label_source_project: Вихідний проект + + text_email_doc_updated_subject: Документи оновлені + text_email_doc_updated: щойно актуалізовані документи від + text_email_doc_follows: наступним чином + text_email_doc_deleted_subject: Документи видалені + text_email_doc_deleted: щойно видалив документи від + label_links_only: лише посилання(лінки) + + label_display_notified_recipients: Відобразити сповіщених отримувачів + note_display_notified_recipients: Користувач буде проінформований про всіх одержувачів щойно надісланого електронного повідомлення + + warning_email_notifications: "Емейл сповіщення надіслані до %{to}" + + link_trash_bin: Корзина + title_restore: Відновити + notice_dmsf_file_restored: Документ успішно відновлено + notice_dmsf_folder_restored: Папку успішно відновлено + notice_dmsf_link_restored: Посилання успішно відновлено + title_restore_checked: Відновлення перевірено + error_parent_folder: "Материнська папка не існує" + + error_resource_or_parent_locked: Не вдалося завершити блокування - ресурс, вихідна (материнська) папка заблокована + error_parent_locked: Не вдалося завершити блокування - материнський ресурс заблоковано + error_resource_locked: Не вдалося завершити блокування - ресурс заблоковано + error_lock_exclusively: Не вдалося заблокувати винятково вже заблокований ресурс + error_unlock_parent_locked: Не вдалося розблокуати - ресурс заблоковано + + label_dmsf_version: Версія + + locked_documents: Заблоковані документи + open_approvals: Відкриті підтвердження + watched_documents: Переглянуті документи + + error_maximum_upload_filecount: "Не більше ніж %{filecount} файла(ів)можуть бути вивантажені." + + label_public_urls: Загальнодоступні URL-адреси дійсні до + + label_webdav: WebDAV + label_full_text: Повнотекстовий пошук + link_extension: Вихід + + label_webdav_ignore: Шаблони ігнорованих файлів + note_webdav_ignore: Типовий вираз із назвами файлів, які ігноруються запитами PUT + + label_document_url: Url + label_last_revision_id: Ревізія + + label_webdav_disable_versioning: Немає керування версіями шаблонами файлів + note_webdav_disable_versioning: Типовий вираз, який вимикає керування версіями для відповідних файлів. Стандартний + шаблон відповідає тимчасовим файлам, створеним MsOffice + + label_dmsf_keep_documents_locked: Залишити окументи заблокованими + note_dmsf_keep_documents_locked: Документи будуть залишатися заблокованими після підтвердження. + note_global: (global) + field_dmsf_not_inheritable: Поле не успадковується + + label_webdav_use_project_names: Використовуйте назви проектів для папок проектів + note_webdav_use_project_names: Використовуйте назви проектів замість ідентифікаторів проектів для папок проектів + + label_last_approver: Останній підтверджувач + + label_act_as_attachable: Діяти як прикріплений + note_dmsf_act_as_attachable: Дозволяє прикріплювати документи до об’єктів, напр. питання. + + label_user_search_add: Пошук користувача для додавання + + label_dmsf_attachments: Вкладення DMS + label_basic_attachments: Базові вкладення + + label_email_from_override: Від + text_email_from_override: Користувач на даний момент увійшов у систему + label_email_reply_to: Відповісти кому + + 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" + + label_dmsf_fast_links: Швидкі посилання + text_dmsf_fast_links_info: Ви зможете вручну ввести ідентифікатор кінцевої папки під час створення посилань або + переміщення файлів чи папок, щоб пришвидшити процес створення посилань. + + label_dmsf_permissions: Дозволити доступ лише + label_inherited_permissions: Успадкований доступ від + + button_edit_content: Редагувати вміст + field_workflow: Робочий потік + field_modified: Дата модифікації + field_updated: Дата оновлення + field_count: D/L + field_first_at: Перший + field_last_at: Останній + field_size: Розмір + field_locked: Заблокований + + label_add_width: Додати з + + dmsf_webdav_ignore_1b_file_for_authentication: Ігнорувати 1б файл, відправлений для аутентифікації + dmsf_webdav_ignore_1b_file_for_authentication_info: Плагін Total Commander WebDAV + + text_not_empty: Папка не порожня. + label_scroll_down: Пролистати донизу + note_webdav_disabled: WebDAV вимкнений. Зверніться до адміністратора. + + dmsf_copy: "Копія (%{n})" + label_empty_trash_bin: Очистити кошик + label_dmsf_projects_as_subfolders: Суб-проекти і суб-папки + note_dmsf_projects_as_subfolders: Додати суб-проекти як суб-папки в перегляд DMS + only_approval_zero_minor_version: Лише схвалення нульової проміжної версії + title_assignment_minor: Призначення не дозволено, значення має дорівнювати нулю + title_start_minor: Початок не дозволений, значення має довівнювати нулю + title_approval_minor: Підтвердження не дозволене, значення має дорівнювати нулю + + label_project_watchers: Хто переглядає + label_dmsf_folder_watchers: Хто переглядає папки + label_dmsf_file_watchers: Хто переглядає файли + label_dmsf_watched: Переглянуті документи + dmsf_legacy_notifications: Застарілі DMS сповіщення + permission_view_dmsf_folder_watchers: Переглянути тих, хто переглядав папку + permission_add_dmsf_folder_watchers: Додати тих, хто переглядав папку + permission_delete_dmsf_folder_watchers: Видалити тих, хто переглядав папку + permission_view_dmsf_file_watchers: Переглнути тих, хто переглядав документ + permission_add_dmsf_file_watchers: Додати тих, хто переглядав документ + permission_delete_dmsf_file_watchers: Видалити тих, хто переглядав документ + permission_view_project_watchers: Переглянути тих, хто переглядав проект + permission_add_project_watchers: Додати тих, хто переглядав проект + permission_delete_project_watchers: Видалити тих, хто переглядав проект + label_dmsf_new_top_level_document: Новий документ DMS топ рівня + label_dmsf_new_top_level_folder: Нова папка DMS топ рівня + + label_dmsf_max_notification_receivers_info: Інформація про отримувачів максимальної кількості сповіщень + note_dmsf_max_notification_receivers_info: Обмежує максимальну кількість відображених отримувачів сповіщень електронною поштою + label_dmsf_office_bin: Libreffice двійковий файл + note_dmsf_office_bin: Двійковий файл для перетворення документів формату офіс у формат PDF і забезпечення їх + попереднього перегляду. Якщо ви хочете заборонити попередній перегляд документів формату офіс, поставте тут порожній + рядок. Після зміни, можливо, доведеться перезапустити програму, щоб вона почала працювати. + + note_dmsf_office_bin_not_available: "LibreOffice's Команда не доступна '%{value}'" + + label_dmsf_columns: Колонки DMS + label_column_id: ІД + label_column_title: Назва + label_column_size: Розмір + label_column_modified: Змінено + label_column_version: Версія + label_column_workflow: Workflow - Робочий процес + label_column_author: Автор + label_column_description: Опис + label_column_comment: Коментар + + label_dmsf_global_menu_disabled: Глобальне меню DMS вимкнене + note_dmsf_global_menu_disabled: Якщо так, об'єкт меню DMS не присутній в топ меню + error_dmsf_workflow_assigned: Робочий процес затвердження, який використовується, не можна ні редагувати, ні видаляти + + label_empty_minor_version_by_default: Порожня версія за замовчуванням + text_email_doc_downloaded_subject: Документи завантажені + text_email_doc_downloaded: Щойно завантажив документи від + field_default_dmsf_query: Запит DMS за замовчуванням + field_receive_download_notification: Отримувати сповіщення про завантаження + + label_remove_original_documents_module: Видалити модуль оригінальних документів + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: Містить недійсні символи diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml new file mode 100644 index 00000000..6ec983a7 --- /dev/null +++ b/config/locales/zh-TW.yml @@ -0,0 +1,497 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman , Aecho Liu +# +# 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 +# . + +zh-TW: + dmsf: 文件總管 # Custom fields tab title + label_dmsf_file: DMS Document + label_dmsf_file_plural: DMS 文件檔案 # Email subject & Search options + label_dmsf_file_revision_plural: Document revisions + label_dmsf_file_revision_access_plural: Document accesses + warning_no_entries_selected: 尚未選取任何項目 + error_email_to_must_be_entered: 請輸入收件者的電子郵件 + warning_file_already_locked: 檔案己經鎖定 + notice_file_locked: 檔案鎖定了 + warning_file_not_locked: 檔案尚未鎖定 + notice_file_unlocked: 檔案己解除鎖定 + error_only_user_that_locked_file_can_unlock_it: 只有檔案的鎖定者,才能解除鎖定。 + + error_max_files_exceeded: "目前容許同時的檔案下載數量為%{number}個,己經超出此限制了。" + error_entry_project_does_not_match_current_project: 所指定的專案,和目前的專案不一致。 + + notice_folder_created: 資料夾己建立 + error_folder_creation_failed: 資料夾建立失敗 + error_folder_title_must_be_entered: 請輸入標題 + notice_folder_deleted: 資料夾己刪除 + error_folder_title_is_already_used: 標題己經被使用了 + notice_folder_details_were_saved: 資料夾描述己儲存 + error_folder_is_locked: 資料夾己經鎖定 + error_file_is_locked: 檔案己經被鎖定了 + notice_file_deleted: 檔案己經被刪除了 + error_at_least_one_revision_must_be_present: 至少要有一個修訂版本存在 + notice_revision_deleted: 修訂版本己經被刪除 + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: 其中一個檔案被鎖定了 + notice_file_revision_created: 檔案修訂版本己建立 + notice_your_preferences_were_saved: 您的偏好設定己經儲存 + notice_your_preferences_were_not_saved: Your preferences were not saved + warning_folder_notifications_already_activated: 資料夾通知己經啟用了 + + notice_folder_notifications_activated: 資料夾通知啟用了 + warning_folder_notifications_already_deactivated: 資料夾通知己經關閉了 + + notice_folder_notifications_deactivated: 資料夾通知關閉了 + warning_file_notifications_already_activated: 檔案通知己經啟用了 + + notice_file_notifications_activated: 檔案通知己啟用 + warning_file_notifications_already_deactivated: 檔案通知己經關閉了 + + notice_file_notifications_deactivated: 檔案通知己關閉 + link_details: "%{title} 描述" + link_edit: "編輯 %{title}" + link_create_folder: 建立資料夾 + link_title: 標題 + link_size: 檔案大小 + link_modified: 修改日期 + link_ver: 版本 + link_author: 作者 + title_check_for_zip_download_or_email: 選取檔案 (下載或電子郵件發送) + title_check_for_restore_or_delete: Check for restore or delete + + title_notifications_active_deactivate: "通知啟用中: 點擊關閉通知" + title_notifications_not_active_activate: "通知關閉中: 點擊啟用通知" + title_title_version_version_download: " 版本: %{version} 檔案: %{title} 下載" + title_locked_by_user: "被使用者%{user},鎖定了。" + title_waiting_for_approval: 等待批准 + title_approved: 已經被批准 + title_unlock_file: 解除鎖定。允許其它使用者修改。 + title_lock_file: 鎖定檔案。禁止其它使用者修改。 + title_download_checked: 以ZIP下載所選取的檔案 + title_send_checked_by_email: 以電子郵件發送所選取的檔案 + link_user_preferences: 你的DMS的系統偏好設定 + heading_send_documents_by_email: 電子郵件寄送檔案 + label_email_from: 寄件者 + label_email_to: 收件者 + label_email_cc: 副本 + label_email_subject: 主題 + label_email_documents: 附加檔案 + label_email_body: 郵件內容 + label_email_send: 寄信囉 + title_notifications_active: 通知處於啟用中 + label_upload: 上傳 + heading_new_folder: 新增資料夾 + label_title: 標題 + label_description: 描述 + submit_save: 儲存 + info_file_locked: 檔案己被鎖定! + label_notifications: 通知 + select_option_default: 預設 + select_option_deactivated: 關閉 + select_option_activated: 啟用 + label_title_format: Title format + text_title_format: "Document title format for download (%t - title, %f - file, %d - date, %v - version, %i - ID, %r - + revision). Example: %t_%v" + title_save_preferences: 儲存偏好設定 + heading_revisions: 修訂版本 + title_download: 下載 + title_delete_revision: 刪除此修訂版本 + title_obsolete_revision: Obsolete revision + label_created: 建立者/時間 + label_changed: 修改者/時間 + info_changed_by_user: "%{changed} by" + label_filename: 檔案名稱 + label_mime: Mime + label_size: 大小 + heading_new_revision: 新增修訂版本 + option_version_same: Same + option_version_patch: Patch + option_version_minor: Minor + option_version_major: Major + option_version_custom: Custom + label_new_content: 新的檔案內容 + label_maximum_files_download: 最大下載檔案數量 + note_maximum_number_of_files_downloaded: 同時間所能下載的,最大檔案數量限制,(下載或電子郵件發送)。 0 表示沒有限制。 + + label_file_storage_directory: 檔案儲存目錄 + label_index_database: 索引資料庫 + label_stemming_language: Stemming language + note_possible_values: 容許的參數 + note_pass_none_to_disable_stemming: "使用 'none' 來停用 stemming" + label_stem_strategy: Stem strategy + option_stem_none: Stem none (default) + option_stem_some: Stem some + option_stem_all: Stem all + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: 檔案通知預設 + heading_uploaded_files: 上傳檔案 + link_documents: 文件檔案 + permission_view_dmsf_file_revision_accesses: View downloads in Activity stream + permission_view_dmsf_file_revisions: View revisions in Activity stream + permission_view_dmsf_folders: 瀏覽文件檔案 + permission_user_preferences: 使用者偏好設定 + permission_view_dmsf_files: 查看文件檔案 + permission_folder_manipulation: 資料夾操作 + permission_file_manipulation: 文件操作 + permission_force_file_unlock: 強制解除檔案鎖定 + permission_manage_workflows: Manage workflows + permission_file_delete: Delete documents + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: 檔案 + field_folder: 資料夾 + error_file_commit_require_uploaded_file: 檔案提交,需要上傳檔案。 + + warning_some_files_were_not_committed: "部份檔案因為驗証失敗無法提交: %{files}" + + error_user_has_not_right_delete_folder: 使用者沒有權限,刪除資料夾。 + + error_user_has_not_right_delete_file: 使用者沒有權限,刪除檔案。 + + notice_entries_deleted: 項目己刪除 + warning_some_entries_were_not_deleted: "部份項目無法刪除: %{entries}" + title_delete_checked: 刪除選取項目 + title_items: items + title_filename_for_download: 下載時的檔名,或是ZIP的檔案名稱。 + label_number_of_folders: 資料夾 + label_number_of_documents: 文件檔案 + error_file_storage_directory_does_not_exist: 資料夾不存在,而且無法被建立。 + + error_file_can_not_be_created: 在存儲目錄裡,無法建立檔案。 + error_wrong_zip_encoding: 不正確的Zip encoding + warning_xapian_not_available: Xapian not available + menu_dmsf: 文件總管 # Project tab title + label_physical_file_delete: 永久刪除檔案 + user_is_not_project_member: 您不是專案的成員之一 + heading_access_downloads_emails: 下載/電子郵件 + heading_access_first: First + heading_access_last: Last + label_dmsf_updated: Updated + label_dmsf_downloaded: Downloaded + title_total_size_of_all_files: 資料夾所有檔案的檔案大小 + project_module_dmsf: 文件總管 # Project module name + warning_no_project_to_copy_file_to: No project to copy file to + comment_copied_from: "Copied from %{source}" + field_target_project: Target project + field_target_folder: Target folder + title_copy_or_move: Copy/Move + label_dmsf_folder_plural: DMS Folders # Search options + comment_moved_from: "Moved from %{source}" + error_target_folder_same: Target folder and project are the same as the current one. + title_copy: Copy + + error_max_email_filesize_exceeded: "己經超出Email所能允許的最大檔案大小。 (%{number} MB)" + + note_maximum_email_filesize: 限制Email所允許的最大檔案大小。 0 表示沒有限制。 + + label_maximum_email_filesize: Email附加檔案最大的檔案大小。 + header_minimum_filesize: 檔案錯誤 + error_minimum_filesize: "這個檔案 %{file} 為空。無法被附加。" + parent_directory: 上層目錄 + note_webdav: "Webdav 啟用後,可在下列網址被找到: %{protocol}://%{domain}/dmsf/webdav/[project identifier]" + + label_copy_dmsf: "複制文件總管的檔案和資料夾。 ( 資料夾: %{folders} 檔案: %{files} )" + label_copy_only_dmsf_folders: "複制文件總管的檔案和資料夾。 (%{folders})" + warning_folder_already_locked: 這個資料夾己經被鎖定了 + notice_folder_locked: 這個資料夾己經成功地鎖定了。 + warning_folder_not_locked: 不好意思,這個資料夾無法被鎖定。 + notice_folder_unlocked: 這個資料夾己經成功地解除鎖定了。 + error_only_user_that_locked_folder_can_unlock_it: 您未被授權,解除這個資料夾的鎖定狀態。 + + title_unlock_folder: 解除鎖定。允許其它使用者修改。 + title_lock_folder: 鎖定資料夾。禁止其它使用者修改。 + + select_option_webdav_readonly: 唯讀 + select_option_webdav_readwrite: 可讀可寫 + label_webdav_strategy: Webdav策略 + + note_webdav_strategy: 讓管理者決定,webdav給使用者的權限,是唯讀or可讀可寫。 + + + error_unable_delete_dmsf_workflow: 無法刪除工作流程 + error_empty_note: "備註不可以為空。" + error_workflow_assign: 當指派時發生錯誤 + error_cannot_start_workflow: "工作流程無法開始" + error_cannot_renumber_steps: "工作步驟無法重新標記號碼" + label_dmsf_workflow_new: 建立批准流程 + field_label_dmsf_workflow: 批准流程 + field_label_dmsf_workflow_name: 批准流程名稱 + label_dmsf_workflow_plural: 批准流程 + label_dmsf_workflow_plural_num: 批准流程 (%{count}) + label_dmsf_workflow_step: 步驟 + label_dmsf_workflow_step_plural: 步驟 + label_dmsf_workflow_approval_plural: 批准 + label_dmsf_wokflow_action_approve: 批准 + label_dmsf_wokflow_action_reject: 拒絕 + label_dmsf_wokflow_action_delegate: 委派給 + label_dmsf_wokflow_action_assign: 指定批准流程 + label_dmsf_wokflow_action_start: 流程開始 + label_dmsf_workflow_add_approver: "Add a new approver with a logical function:" + label_or: or + label_action: Action + label_note: Note + title_none: None + title_rejection: Rejection + title_delegation: Delegation + title_assignment: Assignment + title_start: Start + title_dmsf_workflow_log: 批准流程Log + title_assigned: Assigned + title_approval: Approval + title_rejected: Rejected + title_obsolete: Obsolete + dmsf_and: AND + dmsf_or: OR + dmsf_new_step: New step + dmsf_new_step_or_approver: New step or New approver + message_dmsf_wokflow_note: Your note... + info_revision: "r %{rev}" + link_workflow: Workflow + notice_workflow_started: Approval workflow successfully started + text_email_subject_approved: approved + text_email_subject_rejected: rejected + text_email_subject_delegated: delegated + text_email_subject_requires_approval: requires your approval + text_email_subject_updated: updated + text_email_subject_started: started + text_email_finished_approved: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + finished and the document has been approved." + text_email_finished_rejected: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + finished and the document has been rejected because of '%{notice}'." + text_email_finished_delegated: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + delegated because of '%{notice}' and you are expected to do an approval in the current approval step '%{stepname}'." + text_email_finished_step: "The approval workflow '%{name}' assigned to '%{filename}' document has just finished one of + the approval steps and you are expected to do an approval in the next approval step." + text_email_finished_step_short: "The approval workflow '%{name}' assigned to '%{filename}' document has just finished + one of the approval steps." + text_email_started: "The approval workflow '%{name}' assigned to '%{filename}' document has just been started and you + are expected to do an approval in the current approval step '%{stepname}'." + text_email_to_proceed: To proceed click on the check box icon next to the document in + text_email_to_see_history: To see the approval history click on the workflow status of the document in + + text_email_to_see_status: To see the current status of the approval workflow click on the workflow status the document + in + + title_create_link: Create a symbolic link + label_link_from: Link from + label_link_to: Link to + label_notifications_off: "关闭通知" + label_notifications_on: "开启通知" + field_target_file: Source file + title_download_entries: Download entries + label_external: External + label_internal: Internal + + label_link_name: Link name + field_external_url: URL + label_target_folder: Target folder + label_source_folder: Source folder + label_target_project: Target project + label_source_project: Source project + + text_email_doc_updated_subject: Documents updated + text_email_doc_updated: has just actualized documents of + text_email_doc_follows: as follows + text_email_doc_deleted_subject: Documents deleted + text_email_doc_deleted: has just deleted documents of + label_links_only: links only + + label_display_notified_recipients: Display notified recipients + note_display_notified_recipients: The user will be informed about all recipients of just sent the email notification. + + warning_email_notifications: "Email notifications sent to %{to}" + + link_trash_bin: Trash bin + title_restore: Restore + notice_dmsf_file_restored: The document has been successfully restored + notice_dmsf_folder_restored: The folder has been successfully restored + notice_dmsf_link_restored: The link has been successfully restored + title_restore_checked: Restore checked + error_parent_folder: "The parent folder doesn't exist" + + error_resource_or_parent_locked: Unable to complete lock - resource (or parent) is locked + error_parent_locked: Unable to complete lock - resource parent is locked + error_resource_locked: Unable to complete lock - resource is locked + error_lock_exclusively: Unable to lock exclusively an already-locked resource + error_unlock_parent_locked: Unlock failed - resource parent is locked + + label_dmsf_version: 版本 + + locked_documents: Locked documents + open_approvals: Open approvals + watched_documents: Watched documents + + error_maximum_upload_filecount: "No more than %{filecount} file(s) can be uploaded." + + label_public_urls: Public URLs valid to + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Ignored files patterns + note_webdav_ignore: A regular expresion with filenames to ignore by PUT requests. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. The default pattern + matches temporary files created by MsOffice. + + label_dmsf_keep_documents_locked: Keep documents locked + note_dmsf_keep_documents_locked: Documents will be kept locked when approved + note_global: (global) + field_dmsf_not_inheritable: Not inheritable + + label_webdav_use_project_names: Use project name for project folder + note_webdav_use_project_names: Use project names instead of project identifier for project folders. + + label_last_approver: Last approver + + label_act_as_attachable: Act as attachable + note_dmsf_act_as_attachable: Allows to attach documents to objects e.g. issues. + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: 流程 + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: 大小 + field_locked: Locked + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: 標題 + label_column_size: 大小 + label_column_modified: 已修改 + label_column_version: 版本 + label_column_workflow: 流程 + label_column_author: 作者 + label_column_description: 描述 + label_column_comment: 回應 + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: 內有非法字元 \ No newline at end of file diff --git a/config/locales/zh.yml b/config/locales/zh.yml new file mode 100644 index 00000000..82f65f53 --- /dev/null +++ b/config/locales/zh.yml @@ -0,0 +1,498 @@ +# +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +zh: + dmsf: 文档管家 # Custom fields tab title + label_dmsf_file: DMS Document + label_dmsf_file_plural: DMS 文件檔案 # Email subject & Search options + label_dmsf_file_revision_plural: Document revisions + label_dmsf_file_revision_access_plural: Document accesses + warning_no_entries_selected: 未选择任何条目 + error_email_to_must_be_entered: 请输入电子邮件 + warning_file_already_locked: 文件已经锁定 + notice_file_locked: 文件锁定 + warning_file_not_locked: 文件未锁定 + notice_file_unlocked: 文件解锁 + error_only_user_that_locked_file_can_unlock_it: 只有锁定文件的用户才能解锁该文件 + + error_max_files_exceeded: "超出同时下载%{number}个文件数量限制" + error_entry_project_does_not_match_current_project: 入口项目与当前项目不匹配 + + notice_folder_created: 文件夹创建完毕 + error_folder_creation_failed: 文件夹创建失败 + error_folder_title_must_be_entered: 请输入主题 + notice_folder_deleted: 文件夹已删除 + error_folder_title_is_already_used: 标题已经被使用 + notice_folder_details_were_saved: 文件夹详细信息已保存 + error_folder_is_locked: 資料夾己經鎖定 + error_file_is_locked: 文件被锁定 + notice_file_deleted: 文件已删除 + error_at_least_one_revision_must_be_present: 至少一个修订版本必须存在 + notice_revision_deleted: 修订版本已删除 + notice_revision_obsoleted: Revision obsoleted + warning_one_of_files_locked: 其中一个文件被锁定 + notice_file_revision_created: 文件修订版本已创建 + notice_your_preferences_were_saved: 您的偏好设定已保存 + notice_your_preferences_were_not_saved: Your preferences were not saved + warning_folder_notifications_already_activated: 文件夹通知已激活 + + notice_folder_notifications_activated: 文件夹通知激活 + warning_folder_notifications_already_deactivated: 文件夹通知已注销 + + notice_folder_notifications_deactivated: 文件夹通知注销 + warning_file_notifications_already_activated: 文件通知已激活 + + notice_file_notifications_activated: 文件通知激活 + warning_file_notifications_already_deactivated: 文件通知已注销 + + notice_file_notifications_deactivated: 文件通知注销 + link_details: "%{title} 详情" + link_edit: "编辑 %{title}" + link_create_folder: 创建文件夹 + link_title: 主题 + link_size: 大小 + link_modified: 修改日期 + link_ver: 版本. + link_author: 作者 + title_check_for_zip_download_or_email: 选中用于zip下载或邮件发送 + title_check_for_restore_or_delete: Check for restore or delete + + title_notifications_active_deactivate: "通知有效:点击注销通知" + title_notifications_not_active_activate: "通知无效:点击激活通知" + title_title_version_version_download: " 下载‘%{title}’版本‘%{version}’" + title_locked_by_user: "%{user}锁定" + title_waiting_for_approval: 待批准 + title_approved: 已批准 + title_unlock_file: 解除锁定允许其他成员修改 + title_lock_file: 锁定以防其他成员修改 + title_download_checked: zip归档下载所选 + title_send_checked_by_email: 电子邮件发送所选 + link_user_preferences: 您的文档管理系统项目偏好设定 + heading_send_documents_by_email: 电子邮件发送文档 + label_email_from: 发件人 + label_email_to: 收件人 + label_email_cc: 抄送 + label_email_subject: 主题 + label_email_documents: 文档 + label_email_body: 正文 + label_email_send: 发送 + title_notifications_active: 通知处于有效状态 + label_upload: 上传 + heading_new_folder: 新建文件夹 + label_title: 标题 + label_description: 描述 + submit_save: 保存 + info_file_locked: 文件已被锁定! + label_notifications: 通知功能 + select_option_default: 默认 + select_option_deactivated: 注销 + select_option_activated: 激活 + label_title_format: Title format + text_title_format: "Document title format for download (%t - title, %f - file, %d - date, %v - version, %i - ID, %r - + revision). Example: %t_%v" + title_save_preferences: 保存偏好设定 + heading_revisions: 修订版本 + title_download: 下载 + title_delete_revision: 删除此修订 + title_obsolete_revision: Obsolete revision + label_created: 创建 + label_changed: 修改 + info_changed_by_user: "%{changed} by" + label_filename: 文件名 + label_mime: Mime + label_size: 大小 + heading_new_revision: 新修订 + option_version_same: Same + option_version_patch: Patch + option_version_minor: Minor + option_version_major: Major + option_version_custom: Custom + label_new_content: 新内容 + label_maximum_files_download: 最大下载文件数 + note_maximum_number_of_files_downloaded: 最大文件下载数量(zip或发送电子邮件方式). 0表示无限制. + + label_file_storage_directory: 文件存储目录 + label_index_database: Index database + label_stemming_language: Stemming language + note_possible_values: Possible values + note_pass_none_to_disable_stemming: "pass 'none' to disable stemming" + label_stem_strategy: Stem strategy + option_stem_none: Stem none (default) + option_stem_some: Stem some + option_stem_all: Stem all + text_stemming_info: "This controls how the query parser will apply the stemming algorithm. The default value is + STEM_NONE. The possible values are: STEM_NONE - Don't perform any stemming, STEM_SOME - Search for stemmed forms + of terms except for those which start with a capital letter, or are followed by certain characters + (currently:'/@<>=*[{\"'), or are used with operators which need positional information. Stemmed terms are prefixed + with 'Z', STEM_ALL - Search for stemmed forms of all words (note: no 'Z' prefix is added)." + label_default_notifications: 文件默认通知 + heading_uploaded_files: 上传文件 + link_documents: 文档 + permission_view_dmsf_file_revision_accesses: View downloads in Activity stream + permission_view_dmsf_file_revisions: View revisions in Activity stream + permission_view_dmsf_folders: 浏览文档 + permission_user_preferences: 用户偏好设定 + permission_view_dmsf_files: 查看文档 + permission_folder_manipulation: 文件夹操作 + permission_file_manipulation: 文件操作 + permission_force_file_unlock: 强制文件解锁 + permission_manage_workflows: Manage workflows + permission_file_delete: Delete documents + permission_display_system_folders: Display system folders + permission_file_approval: File approval + permission_email_documents: Email documents + label_file: 文件 + field_folder: 件夹 + error_file_commit_require_uploaded_file: 文件提交要求上传文件 + + warning_some_files_were_not_committed: "某些文件因验证错误未能被提交: %{files}" + + error_user_has_not_right_delete_folder: 用户没有权限删除文件夹 + + error_user_has_not_right_delete_file: 用户没有权限删除文件 + + notice_entries_deleted: 条目已删除 + warning_some_entries_were_not_deleted: "某些条目未被删除: %{entries}" + title_delete_checked: 删除选中 + title_items: items + title_filename_for_download: 用于下载或zip归档的文件名 + label_number_of_folders: Folders + label_number_of_documents: Documents + error_file_storage_directory_does_not_exist: 文件存储目录不存在或不能创建 + + error_file_can_not_be_created: 文件未能在存储目录中创建 + error_wrong_zip_encoding: 不正确的Zip编码 + warning_xapian_not_available: Xapian not available + menu_dmsf: 文档管家 # Project tab title + label_physical_file_delete: 物理删除文件 + user_is_not_project_member: 您不是该项目的成员 + heading_access_downloads_emails: 存取次数 + heading_access_first: 首次 + heading_access_last: 末次 + label_dmsf_updated: Updated + label_dmsf_downloaded: Downloaded + title_total_size_of_all_files: 文件夹所有文件总大小 + project_module_dmsf: 文档管家 # Project module name + warning_no_project_to_copy_file_to: No project to copy file to + comment_copied_from: "Copied from %{source}" + field_target_project: Target project + field_target_folder: Target folder + title_copy_or_move: Copy/Move + label_dmsf_folder_plural: DMS Folders # Search options + comment_moved_from: "Moved from %{source}" + error_target_folder_same: Target folder and project are the same as the current one. + title_copy: Copy + + error_max_email_filesize_exceeded: "You've exceeded the maximum filesize for sending via email. (%{number} MB)" + + note_maximum_email_filesize: Limits maximum filesize that can be sent via email. 0 means unlimited. Number is in MB. + + label_maximum_email_filesize: Maximum email attachment size + header_minimum_filesize: File Error. + error_minimum_filesize: "The file %{file} is 0 bytes and will not be attached." + parent_directory: Parent Directory + note_webdav: "Webdav once enabled can be found at %{protocol}://%{domain}/dmsf/webdav/[project identifier]" + + label_copy_dmsf: "Copy documents and folders (%{files} files in %{folders} folders)" + label_copy_only_dmsf_folders: "Copy documents only (%{folders})" + + warning_folder_already_locked: This folder is already locked + notice_folder_locked: The folder was successfully locked + warning_folder_not_locked: Unfortunately, the folder could not be locked + notice_folder_unlocked: The folder was successfully unlocked + error_only_user_that_locked_folder_can_unlock_it: You are not authorised to unlock this folder + + title_unlock_folder: Unlock to allow changes for other members + title_lock_folder: Lock to prevent changes for other members + + select_option_webdav_readonly: Read-only + select_option_webdav_readwrite: Read/Write + label_webdav_strategy: Webdav strategy + + note_webdav_strategy: Enables the administrator to decide if webdav is a read-only or read-write platform for end + users. + + error_unable_delete_dmsf_workflow: Unable to delete the workflow + error_empty_note: "The note can't be empty" + error_workflow_assign: An error occured while assigning + error_cannot_start_workflow: "Workflow can't be started" + error_cannot_renumber_steps: "Steps can't be renumbered" + label_dmsf_workflow_new: New approval workflow + field_label_dmsf_workflow: Approval Workflow + field_label_dmsf_workflow_name: Approval workflow name + label_dmsf_workflow_plural: Approval workflows + label_dmsf_workflow_plural_num: Copy approval workflows (%{count}) + label_dmsf_workflow_step: Step + label_dmsf_workflow_step_plural: Steps + label_dmsf_workflow_approval_plural: Approvals + label_dmsf_wokflow_action_approve: Approve + label_dmsf_wokflow_action_reject: Reject + label_dmsf_wokflow_action_delegate: Delegate to + label_dmsf_wokflow_action_assign: Assign an approval workflow + label_dmsf_wokflow_action_start: Start workflow + label_dmsf_workflow_add_approver: "Add a new approver with a logical function:" + label_or: or + label_action: Action + label_note: Note + title_none: None + title_rejection: Rejection + title_delegation: Delegation + title_assignment: Assignment + title_start: Start + title_dmsf_workflow_log: Approval Workflow Log + title_assigned: Assigned + title_approval: Approval + title_rejected: Rejected + title_obsolete: Obsolete + dmsf_and: AND + dmsf_or: OR + dmsf_new_step: New step + dmsf_new_step_or_approver: New step or New approver + message_dmsf_wokflow_note: Your note... + info_revision: "r %{rev}" + link_workflow: Workflow + notice_workflow_started: Approval workflow successfully started + text_email_subject_approved: approved + text_email_subject_rejected: rejected + text_email_subject_delegated: delegated + text_email_subject_requires_approval: requires your approval + text_email_subject_updated: updated + text_email_subject_started: started + text_email_finished_approved: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + finished and the document has been approved." + text_email_finished_rejected: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + finished and the document has been rejected because of '%{notice}'." + text_email_finished_delegated: "The approval workflow '%{name}' assigned to '%{filename}' document has just been + delegated because of '%{notice}' and you are expected to do an approval in the current approval step '%{stepname}'." + text_email_finished_step: "The approval workflow '%{name}' assigned to '%{filename}' document has just finished one of + the approval steps and you are expected to do an approval in the next approval step." + text_email_finished_step_short: "The approval workflow '%{name}' assigned to '%{filename}' document has just finished + one of the approval steps." + text_email_started: "The approval workflow '%{name}' assigned to '%{filename}' document has just been started and you + are expected to do an approval in the current approval step '%{stepname}'." + text_email_to_proceed: To proceed click on the check box icon next to the document in + text_email_to_see_history: To see the approval history click on the workflow status of the document in + + text_email_to_see_status: To see the current status of the approval workflow click on the workflow status the document + in + + title_create_link: Create a symbolic link + label_link_from: Link from + label_link_to: Link to + label_notifications_off: "关闭通知" + label_notifications_on: "开启通知" + field_target_file: Source file + title_download_entries: Download entries + label_external: External + label_internal: Internal + + label_link_name: Link name + field_external_url: URL + label_target_folder: Target folder + label_source_folder: Source folder + label_target_project: Target project + label_source_project: Source project + + text_email_doc_updated_subject: Documents updated + text_email_doc_updated: has just actualized documents of + text_email_doc_follows: as follows + text_email_doc_deleted_subject: Documents deleted + text_email_doc_deleted: has just deleted documents of + label_links_only: links only + + label_display_notified_recipients: Display notified recipients + note_display_notified_recipients: The user will be informed about all recipients of just sent the email notification. + + warning_email_notifications: "Email notifications sent to %{to}" + + link_trash_bin: Trash bin + title_restore: Restore + notice_dmsf_file_restored: The document has been successfully restored + notice_dmsf_folder_restored: The folder has been successfully restored + notice_dmsf_link_restored: The link has been successfully restored + title_restore_checked: Restore checked + error_parent_folder: "The parent folder doesn't exist" + + error_resource_or_parent_locked: Unable to complete lock - resource (or parent) is locked + error_parent_locked: Unable to complete lock - resource parent is locked + error_resource_locked: Unable to complete lock - resource is locked + error_lock_exclusively: Unable to lock exclusively an already-locked resource + error_unlock_parent_locked: Unlock failed - resource parent is locked + + label_dmsf_version: 版本 + + locked_documents: Locked documents + open_approvals: Open approvals + watched_documents: Watched documents + + error_maximum_upload_filecount: "No more than %{filecount} file(s) can be uploaded." + + label_public_urls: Public URLs valid to + + label_webdav: WebDAV + label_full_text: Full-text search + link_extension: Ext + + label_webdav_ignore: Ignored files patterns + note_webdav_ignore: A regular expresion with filenames to ignore by PUT requests. + + label_document_url: Url + label_last_revision_id: Revision + + label_webdav_disable_versioning: No versioning files patterns + note_webdav_disable_versioning: A regular expression that disables versioning for matching files. The default pattern + matches temporary files created by MsOffice. + + label_dmsf_keep_documents_locked: Keep documents locked + note_dmsf_keep_documents_locked: Documents will be kept locked when approved + note_global: (global) + field_dmsf_not_inheritable: Not inheritable + + label_webdav_use_project_names: Use project name for project folder + note_webdav_use_project_names: Use project names instead of project identifier for project folders. + + label_last_approver: Last approver + + label_act_as_attachable: Act as attachable + note_dmsf_act_as_attachable: Allows to attach documents to objects e.g. issues. + + label_user_search_add: Search for user to add + + label_dmsf_attachments: DMS Attachments + label_basic_attachments: Basic Attachments + + label_email_from_override: From + text_email_from_override: The user currently logged in + label_email_reply_to: Reply-to + + 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" + + 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 + or folders in order to speed up the process of creating links. + + label_dmsf_permissions: Allow access only to + label_inherited_permissions: Inherited Access for + + button_edit_content: Edit content + field_workflow: 工作流程 + field_modified: Date + field_updated: Date + field_count: D/L + field_first_at: First + field_last_at: Last + field_size: 大小 + field_locked: Locked + + label_add_width: Add with + + dmsf_webdav_ignore_1b_file_for_authentication: Ignore 1b file sent for authentication + dmsf_webdav_ignore_1b_file_for_authentication_info: Total Commander WebDAV plugin + + text_not_empty: The folder is not empty. + label_scroll_down: Scroll down + note_webdav_disabled: WebDAV is disabled. Contact the administrator. + + dmsf_copy: "Copy (%{n})" + label_empty_trash_bin: Empty Trash + label_dmsf_projects_as_subfolders: Sub-projects as sub-folders + note_dmsf_projects_as_subfolders: Add sub-projects as sub-folders into DMS view + only_approval_zero_minor_version: Only approval zero minor version + title_assignment_minor: Assignment not allowed, minor must be zero + title_start_minor: Start not allowed, minor must be zero + title_approval_minor: Approval not allowed, minor must be zero + + label_project_watchers: Watchers + label_dmsf_folder_watchers: Watchers + label_dmsf_file_watchers: Watchers + label_dmsf_watched: Watched documents + dmsf_legacy_notifications: Legacy DMS notifications + permission_view_dmsf_folder_watchers: View folder's watchers + permission_add_dmsf_folder_watchers: Add folder's watchers + permission_delete_dmsf_folder_watchers: Delete folder's watchers + permission_view_dmsf_file_watchers: View document's watchers + permission_add_dmsf_file_watchers: Add document's watchers + permission_delete_dmsf_file_watchers: Delete document's watchers + permission_view_project_watchers: View project's watchers + permission_add_project_watchers: Add project's watchers + permission_delete_project_watchers: Delete project's watchers + label_dmsf_new_top_level_document: New top level DMS document + label_dmsf_new_top_level_folder: New top level DMS folder + + label_dmsf_max_notification_receivers_info: Maximum notification receivers info + note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers. + label_dmsf_office_bin: Libreoffice binary + note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want + to prevent previews of office documents, put an empty string here. After a change, you might have to restart the + application to take it any effect. + note_dmsf_office_bin_not_available: "LibreOffice's command line binary '%{value}' not available" + + label_dmsf_columns: DMS Columns + label_column_id: ID + label_column_title: 标题 + label_column_size: 大小 + label_column_modified: 已修改 + label_column_version: 版本 + label_column_workflow: 工作流程 + label_column_author: 作者 + label_column_description: 描述 + label_column_comment: 注释 + + label_dmsf_global_menu_disabled: Global DMS menu disabled + note_dmsf_global_menu_disabled: If yes, DMS menu item is not present in the top menu. + error_dmsf_workflow_assigned: Approval workflow in use can be neither edited nor deleted. + + label_empty_minor_version_by_default: Empty minor version by default + text_email_doc_downloaded_subject: Documents downloaded + text_email_doc_downloaded: has just downloaded documents of + field_default_dmsf_query: Default DMS query + field_receive_download_notification: Receive download notifications + + label_remove_original_documents_module: Remove the original Documents module + + notice_entries_copied: Copying has succeeded + notice_entries_moved: Moving has succeeded + label_dmsf_file_revision: DMS Document rev. + error_not_supported_image_format: Not supported image format + error_not_supported_video_format: Not supported video format + + label_webdav_authentication: WebDAV Authentication + note_webdav_authentication: Basic authentication method is considered as unsecure and therefore blocked by some + clients. Digest authentication is based on an auto-generated digest. Users use their login and password for + authentication in their WebDAV clients too. + label_dmsf_webdav_digest_created_on: "DMS WebDAV digest created %{value} ago" + label_missing_dmsf_webdav_digest: Missing a DMS WebDAV digest + label_dmsf_webdav_digest: DMS WebDAV digest + text_dmsf_webdav_digest_reset: You are supposed to enter your password to generate a new DMS WebDAV digest. + notice_webdav_digest_reset: Your DMS WebDAV digest was reset. + + label_dmsf_commit: Commit + label_dmsf_upload_commit: Upload and commit + + notice_search_in_subfolders: Searching in sub-folders is not recursive. For a recursive search go to the top level. + warning_folder_unlockable: The folder can't be unlocked + redmine_dmsf: Redmine DMSF + + activerecord: + errors: + messages: + error_contains_invalid_character: 含有无效字符 \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 00000000..2b3715fc --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +if Redmine::Plugin.installed? 'redmine_dmsf' + RedmineApp::Application.routes.draw do + # + # dmsf controller + # /projects//dmsf + # [As this controller also processes 'folders' it maybe better to branch into a folder route rather than leaving + # it as is] + ## + post '/projects/:id/dmsf/create', controller: 'dmsf', action: 'create' + get '/projects/:id/dmsf/notify/activate', controller: 'dmsf', action: 'notify_activate', as: 'notify_activate_dmsf' + get '/projects/:id/dmsf/notify/deactivate', controller: 'dmsf', + action: 'notify_deactivate', + as: 'notify_deactivate_dmsf' + delete '/projects/:id/dmsf/delete', controller: 'dmsf', action: 'delete', as: 'delete_dmsf' + post '/projects/:id/dmsf/save', controller: 'dmsf', action: 'save' + post '/projects/:id/dmsf/save/root', controller: 'dmsf', action: 'save_root' + post '/projects/:id/dmsf/entries', controller: 'dmsf', action: 'entries_operation', as: 'entries_operations_dmsf' + post '/projects/:id/dmsf/entries/delete', controller: 'dmsf', action: 'delete_entries', as: 'delete_entries' + post '/projects/:id/dmsf/entries/email', to: 'dmsf#entries_email', as: 'email_entries' + get '/projects/:id/dmsf/entries/:entry/download_email_entries', controller: 'dmsf', + action: 'download_email_entries', + as: 'download_email_entries' + get '/projects/:id/entries/copymove', to: 'dmsf#copymove', as: 'copymove_entries' + get '/projects/:id/dmsf/lock', controller: 'dmsf', action: 'lock', as: 'lock_dmsf' + get '/projects/:id/dmsf/unlock', controller: 'dmsf', action: 'unlock', as: 'unlock_dmsf' + get '/projects/:id/dmsf/', controller: 'dmsf', action: 'show', as: 'dmsf_folder' + get '/projects/:id/dmsf/new', controller: 'dmsf', action: 'new', as: 'new_dmsf' + get '/projects/:id/dmsf/edit', controller: 'dmsf', action: 'edit', as: 'edit_dmsf' + get '/projects/:id/dmsf/edit/root', controller: 'dmsf', action: 'edit_root', as: 'edit_root_dmsf' + get '/projects/:id/dmsf/trash', controller: 'dmsf', action: 'trash', as: 'trash_dmsf' + get '/projects/:id/dmsf/restore', controller: 'dmsf', action: 'restore', as: 'restore_dmsf' + post '/projects/dmsf/expand_folder', controller: 'dmsf', action: 'expand_folder', as: 'expand_folder_dmsf' + get '/projects/dmsf/add_email', to: 'dmsf#add_email', as: 'add_email_dmsf' + post '/projects/dmsf/append_email', to: 'dmsf#append_email', as: 'append_email_dmsf' + get '/projects/dmsf/autocomplete_for_user', to: 'dmsf#autocomplete_for_user' + put '/projects/:id/dmsf', controller: 'dmsf', action: 'drop' + delete '/projects/:id/dmsf/empty_trash', to: 'dmsf#empty_trash', as: 'empty_trash' + get '/dmsf', to: 'dmsf#index', as: 'dmsf_index' + get '/dmsf/digest', to: 'dmsf#digest', as: 'dmsf_digest' + post '/dmsf/digest', to: 'dmsf#reset_digest', as: 'dmsf_reset_digest' + + # dmsf_context_menu_controller + match '/projects/dmsf/context_menu', to: 'dmsf_context_menus#dmsf', as: 'dmsf_context_menu', via: %i[get post] + match '/projects/:id/dmsf/trash/context_menu', to: 'dmsf_context_menus#trash', + as: 'dmsf_trash_context_menu', + via: %i[get post] + + # + # dmsf_state controller + # /projects//dmsf/state + ## + post '/projects/:id/dmsf/state', controller: 'dmsf_state', action: 'user_pref_save', as: 'dmsf_user_pref_save' + + # + # dmsf_upload controller + # /projects//dmsf/upload - dmsf_upload controller + ## + + get '/projects/:id/dmsf/upload/multi_upload', controller: 'dmsf_upload', + action: 'multi_upload', + as: 'multi_dmsf_upload' + post '/projects/:id/dmsf/upload/files', controller: 'dmsf_upload', action: 'upload_files' + get '/projects/:id/dmsf/upload/files', controller: 'dmsf_upload', action: 'upload_files' + post '/projects/:id/dmsf/upload', controller: 'dmsf_upload', action: 'upload' + post '/projects/:id/dmsf/upload/commit', controller: 'dmsf_upload', action: 'commit_files' + post '/projects/:id/dmsf/commit', controller: 'dmsf_upload', action: 'commit' + post 'dmsf_uploads', to: 'dmsf_upload#upload' + delete '/dmsf/attachments/:id/delete', to: 'dmsf_upload#delete_dmsf_attachment', as: 'dmsf_attachment' + delete '/dmsf/link_attachments/:id/delete', to: 'dmsf_upload#delete_dmsf_link_attachment', + as: 'dmsf_link_attachment' + + # + # dmsf_links controller + # /dmsf/links/ + ## + get '/dmsf/links/:id/restore', controller: 'dmsf_links', action: 'restore', as: 'restore_dmsf_link' + delete '/dmsf/links/:id', controller: 'dmsf_links', action: 'delete' + + # Just to keep backward compatibility with old external direct links + get '/dmsf_files/:id', controller: 'dmsf_files', action: 'show' + get '/dmsf_files/:id/download', controller: 'dmsf_files', action: 'show', download: '' + + # + # dmsf_files controller + # /dmsf/files/ + ## + get '/dmsf/files/:id/notify/activate', controller: 'dmsf_files', + action: 'notify_activate', + as: 'notify_activate_dmsf_files' + get '/dmsf/files/:id/notify/deactivate', controller: 'dmsf_files', + action: 'notify_deactivate', + as: 'notify_deactivate_dmsf_files' + get '/dmsf/files/:id/lock', controller: 'dmsf_files', action: 'lock', as: 'lock_dmsf_files' + get '/dmsf/files/:id/unlock', controller: 'dmsf_files', action: 'unlock', as: 'unlock_dmsf_files' + post '/dmsf/files/:id/delete', controller: 'dmsf_files', action: 'delete', as: 'delete_dmsf_files' + post '/dmsf/files/:id/revision/create', controller: 'dmsf_files', action: 'create_revision' + delete '/dmsf/files/:id/revision/delete', controller: 'dmsf_files', action: 'delete_revision', as: 'delete_revision' + get '/dmsf/files/:id/revision/obsolete', controller: 'dmsf_files', + action: 'obsolete_revision', + as: 'obsolete_revision' + get '/dmsf/files/:id/download', to: 'dmsf_files#view', + download: '', + as: 'download_dmsf_file' # Otherwise will not route nil into the download param + get '/dmsf/files/:id/view', to: 'dmsf_files#view', as: 'view_dmsf_file' + get '/dmsf/files/:id', controller: 'dmsf_files', action: 'show', as: 'dmsf_file' + delete '/dmsf/files/:id', controller: 'dmsf_files', action: 'delete' + get '/dmsf/files/:id/restore', controller: 'dmsf_files', action: 'restore', as: 'restore_dmsf_file' + get '/dmsf/files/:id/thumbnail', to: 'dmsf_files#thumbnail', as: 'dmsf_thumbnail' + get '/dmsf/files/:id/:filename', to: 'dmsf_files#view', id: /\d+/, filename: /.*/, as: 'static_dmsf_file' + + # Approval workflow + resources :dmsf_workflows do + member do + get 'autocomplete_for_user' + get 'action' + get 'assign' + get 'log' + post 'new_action' + get 'start' + post 'assignment' + get 'new_step' + put 'update_step' + delete 'delete_step' + end + end + + post 'dmsf_workflows/:id/edit', controller: 'dmsf_workflows', action: 'add_step', id: /\d+/, via: :post + delete 'dmsf_workflows/:id/edit', controller: 'dmsf_workflows', action: 'remove_step', id: /\d+/, via: :delete + put 'dmsf_workflows/:id/edit', controller: 'dmsf_workflows', action: 'reorder_steps', id: /\d+/, via: :put + + # Links + resources :dmsf_links do + member do + get 'restore' + get 'autocomplete_for_project' + get 'autocomplete_for_folder' + end + end + + # Public URLs + resource :dmsf_public_urls + + # Folder permissions + resource :dmsf_folder_permissions do + member do + get 'autocomplete_for_user' + post 'append' + end + end + + # WebDAV workaround for clients checking WebDAV availability in the root + match '/', + to: ->(env) { [405, {}, ["#{env['REQUEST_METHOD']} method is not allowed"]] }, + via: %i[propfind options] + match '/dmsf', + to: ->(env) { [405, {}, ["#{env['REQUEST_METHOD']} method is not allowed"]] }, + via: %i[propfind options] + + # Help + get '/dmsf/help/wiki_syntax', controller: 'dmsf_help', action: 'show_wiki_syntax', as: 'dmsf_wiki_syntax' + get '/dmsf/help/dmsf_help', controller: 'dmsf_help', action: 'show_dmsf_help', as: 'dmsf_help' + end +end diff --git a/db/migrate/01_create_hierarchy.rb b/db/migrate/01_create_hierarchy.rb new file mode 100644 index 00000000..c0fa102a --- /dev/null +++ b/db/migrate/01_create_hierarchy.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Initial schema +class CreateHierarchy < ActiveRecord::Migration[4.2] + def change + create_table :dmsf_folders do |t| + t.references :project, null: false + t.references :dmsf_folder + t.string :name, null: false + t.text :description + t.boolean :notification, default: false, null: false + t.references :user, null: false + t.timestamps + end + create_table :dmsf_files do |t| + t.references :project, null: false + # This two fileds are copy from last revision due to simpler search + t.references :dmsf_folder + t.string :name, null: false + t.boolean :notification, default: false, null: false + t.boolean :deleted, default: false, null: false + t.integer :deleted_by_user_id + t.timestamps + end + create_table :dmsf_file_revisions do |t| + t.references :dmsf_file, null: false + t.integer :source_dmsf_file_revision_id + t.string :name, null: false + t.references :dmsf_folder + t.string :disk_filename, null: false + t.integer :size + t.string :mime_type + t.string :title + t.text :description + t.integer :workflow + t.integer :major_version, null: false + t.integer :minor_version, null: false + t.text :comment + t.boolean :deleted, default: false, null: false + t.integer :deleted_by_user_id + t.references :user, null: false + t.timestamps + end + create_table :dmsf_file_locks do |t| + t.references :dmsf_file, null: false + t.boolean :locked, default: false, null: false + t.references :user, null: false + t.timestamps + end + create_table :dmsf_user_prefs do |t| + t.references :project, null: false + t.references :user, null: false + t.boolean :email_notify, null: false, default: false + t.timestamps + end + end +end diff --git a/db/migrate/02_dmsf_normalization.rb b/db/migrate/02_dmsf_normalization.rb new file mode 100644 index 00000000..2d9777bc --- /dev/null +++ b/db/migrate/02_dmsf_normalization.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Rename column +class DmsfNormalization < ActiveRecord::Migration[4.2] + def up + rename_column :dmsf_folders, :name, :title + end + + def down + rename_column :dmsf_folders, :title, :name + end +end diff --git a/db/migrate/03_dmsf_0_8_0.rb b/db/migrate/03_dmsf_0_8_0.rb new file mode 100644 index 00000000..f3af3e19 --- /dev/null +++ b/db/migrate/03_dmsf_0_8_0.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Add column +class Dmsf080 < ActiveRecord::Migration[4.2] + def change + add_column :projects, :dmsf_description, :text + end +end diff --git a/db/migrate/04_dmsf_0_9_0.rb b/db/migrate/04_dmsf_0_9_0.rb new file mode 100644 index 00000000..a49f9d7c --- /dev/null +++ b/db/migrate/04_dmsf_0_9_0.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Add column +class Dmsf090 < ActiveRecord::Migration[4.2] + def up + add_column :members, :dmsf_mail_notification, :boolean, + null: false, default: false + drop_table :dmsf_user_prefs + end + + def down + remove_column :members, :dmsf_mail_notification + create_table :dmsf_user_prefs do |t| + t.references :project, null: false + t.references :user, null: false + t.boolean :email_notify, null: false, default: false + t.timestamps + end + end +end diff --git a/db/migrate/05_dmsf_0_9_0_1.rb b/db/migrate/05_dmsf_0_9_0_1.rb new file mode 100644 index 00000000..adc1d47c --- /dev/null +++ b/db/migrate/05_dmsf_0_9_0_1.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Create table +class Dmsf0901 < ActiveRecord::Migration[4.2] + def change + create_table :dmsf_file_revision_accesses do |t| + t.references :dmsf_file_revision, null: false + t.integer :action, default: 0, null: false # 0 ... download, 1 ... email + t.references :user, null: false + t.timestamps null: false + end + end +end diff --git a/db/migrate/06_dmsf_1_2_0.rb b/db/migrate/06_dmsf_1_2_0.rb new file mode 100644 index 00000000..6f27e43a --- /dev/null +++ b/db/migrate/06_dmsf_1_2_0.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +# Add column +class Dmsf120 < ActiveRecord::Migration[4.2] + def up + add_column :dmsf_file_revisions, :project_id, :integer, null: true + DmsfFileRevision.reset_column_information + DmsfFileRevision.find_each do |revision| + if revision.dmsf_file + revision.project_id = revision.dmsf_file.project.id + revision.save! + end + end + change_column :dmsf_file_revisions, :project_id, :integer, null: false + end + + def down + remove_column :dmsf_file_revisions, :project_id + end +end diff --git a/db/migrate/07_dmsf_1_4_4.rb b/db/migrate/07_dmsf_1_4_4.rb new file mode 100644 index 00000000..b5ca5e26 --- /dev/null +++ b/db/migrate/07_dmsf_1_4_4.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require 'fileutils' +require 'uuidtools' + +# Locking +class Dmsf144 < ActiveRecord::Migration[4.2] + # File lock + class DmsfFileLock < ApplicationRecord + belongs_to :file, class_name: 'DmsfFile', foreign_key: 'dmsf_file_id' + belongs_to :user + end + + def up + change_table :dmsf_file_locks, bulk: true do |t| + # Add our entity_type column (used with our entity type) + t.column :entity_type, :integer, null: true + # Add our lock relevent columns (ENUM) - null (till we upgrade data) + t.column :lock_type_cd, :integer, null: true + t.column :lock_scope_cd, :integer, null: true + t.column :uuid, :string, null: true, limit: 36 + # Add our expires_at column + t.column :expires_at, :datetime, null: true + end + do_not_delete = [] + # - 2012-07-12: Better compatibility for postgres - query used 3 columns however + # only on appearing in group, find_each imposes a limit and incorrect + # ordering, so adapted that, we grab id's load a mock object, and reload + # data into it, which should enable us to run checks we need, not as + # efficient, however compatible across the board. + DmsfFileLock.reset_column_information + DmsfFileLock.select('MAX(id), id').order(Arel.sql('MAX(id) DESC')).group(:dmsf_file_id, :id).find do |lock| + lock.reload + do_not_delete << lock.id if lock.locked + end + # Generate new lock Id's for whats being persisted + do_not_delete.each do |l| + # Find the lock + lock = DmsfFileLock.find(l) + next unless lock + + lock.uuid = UUIDTools::UUID.random_create.to_s + lock.save! + end + say "Preserving #{do_not_delete.count} file lock(s) found in old schema" + DmsfFileLock.where.not(id: do_not_delete).delete_all + # We need to force our newly found + say 'Applying default lock scope / type - Exclusive / Write' + DmsfFileLock.update_all entity_type: 0, lock_type_cd: 0, lock_scope_cd: 0 + change_table :dmsf_file_locks, bulk: true do |t| + # These are not null-allowed columns + t.change :entity_type, :integer, null: false + t.change :lock_type_cd, :integer, null: false + t.change :lock_scope_cd, :integer, null: false + # Data cleanup + t.rename :dmsf_file_id, :entity_id + # TODO: The column cannot be removed on SQL server due to NOT NULL constraint. + # The constraint's name is random and therefore cannot be easily removed. + t.remove :locked if ActiveRecord::Base.connection.adapter_name.downcase != 'sqlserver' + end + rename_table :dmsf_file_locks, :dmsf_locks + # Not sure if this is the right place to do this, as its file manipulation, not database (strictly) + say 'Completing one-time file migration ...' + begin + DmsfFileRevision.find_each do |rev| + next unless rev.project + + existing = DmsfFile.storage_path.join rev.disk_filename + new_path = rev.disk_file(search_if_not_exists: false) + begin + if File.exist?(existing) + if File.exist?(new_path) + rev.disk_filename = rev.new_storage_filename + new_path = rev.disk_file(search_if_not_exists: false) + rev.save! + end + # Ensure the project path exists + project_directory = File.dirname(new_path) + Dir.mkdir(project_directory) unless File.directory? project_directory + FileUtils.mv existing, new_path + say "Migration: #{existing} -> #{new_path} succeeded" + end + rescue StandardError => e # Here we wrap around IO in the loop to prevent one failure ruining complete run. + say "Migration: #{existing} -> #{new_path} failed" + Rails.logger.error e.message + end + end + say 'Action was successful' + rescue StandardError => e + say 'Action was not successful' + Rails.logger.error e.message # See issue #86 + # Nothing here, we just dont want a migration to break + end + end + + def down + rename_table :dmsf_locks, :dmsf_file_locks + add_column :dmsf_file_locks, :locked, :boolean, default: false, null: false + # Data cleanup - delete all expired locks, or any folder locks + DmsfFileLock.reset_column_information + say 'Removing all expired and/or folder locks' + DmsfFileLock.where(['expires_at < ? OR entity_type = 1', Time.current]).delete_all + say 'Changing all records to be locked' + DmsfFileLock.update_all locked: true + change_table :dmsf_file_locks, bulk: true do |t| + t.rename :entity_id, :dmsf_file_id + t.remove :entity_type + t.remove :lock_type_cd + t.remove :lock_scope_cd + t.remove :expires_at + t.remove :uuid + end + # Not sure if this is the right place to do this, as its file manipulation, not database (stricly) + begin + say 'restoring old file-structure' + DmsfFileRevision.find_each do |rev| + next unless rev.project + + project = rev.project.identifier.gsub(/[^\w.\-]/, '_') + existing = DmsfFile.storage_path.join("p_#{project}/#{rev.disk_filename}") + new_path = DmsfFile.storage_path.join(rev.disk_filename) + if File.exist?(existing) + if File.exist?(new_path) + rev.disk_filename = rev.new_storage_filename + rev.save! + new_path = DmsfFile.storage_path.join(rev.disk_filename) + end + FileUtils.mv existing, new_path + end + end + rescue StandardError + # Nothing here, we just dont want a migration to break + end + end +end diff --git a/db/migrate/20120822100401_create_dmsf_workflows.rb b/db/migrate/20120822100401_create_dmsf_workflows.rb new file mode 100644 index 00000000..a9f70882 --- /dev/null +++ b/db/migrate/20120822100401_create_dmsf_workflows.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# Karel Pičman +# +# 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 +# . + +# Create table +class CreateDmsfWorkflows < ActiveRecord::Migration[4.2] + def up + create_table :dmsf_workflows do |t| + t.string :name, null: false + t.references :project + t.timestamps + end + add_index :dmsf_workflows, [:name], unique: true + change_table :dmsf_file_revisions, bulk: true do |t| + t.references :dmsf_workflow + t.integer :dmsf_workflow_assigned_by + t.datetime :dmsf_workflow_assigned_at + t.integer :dmsf_workflow_started_by + t.datetime :dmsf_workflow_started_at + end + end + + def down + change_table :dmsf_file_revisions, bulk: true do |t| + t.remove :dmsf_workflow_id + t.remove :dmsf_workflow_assigned_by + t.remove :dmsf_workflow_assigned_at + t.remove :dmsf_workflow_started_by + t.remove :dmsf_workflow_started_at + end + drop_table :dmsf_workflows + end +end diff --git a/db/migrate/20120822100402_create_dmsf_workflow_steps.rb b/db/migrate/20120822100402_create_dmsf_workflow_steps.rb new file mode 100644 index 00000000..1a94bc57 --- /dev/null +++ b/db/migrate/20120822100402_create_dmsf_workflow_steps.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Create table +class CreateDmsfWorkflowSteps < ActiveRecord::Migration[4.2] + def change + create_table :dmsf_workflow_steps do |t| + t.references :dmsf_workflow, null: false + t.integer :step, null: false + t.references :user, null: false + t.integer :operator, null: false + t.datetime :created_at, default: -> { 'CURRENT_TIMESTAMP' } + end + add_index :dmsf_workflow_steps, :dmsf_workflow_id + end +end diff --git a/db/migrate/20120822100403_create_dmsf_workflow_step_assignments.rb b/db/migrate/20120822100403_create_dmsf_workflow_step_assignments.rb new file mode 100644 index 00000000..8f1b07d6 --- /dev/null +++ b/db/migrate/20120822100403_create_dmsf_workflow_step_assignments.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Create table +class CreateDmsfWorkflowStepAssignments < ActiveRecord::Migration[4.2] + def change + create_table :dmsf_workflow_step_assignments do |t| + t.references :dmsf_workflow_step, null: false + t.references :user, null: false + t.references :dmsf_file_revision, null: false + t.datetime :created_at, default: -> { 'CURRENT_TIMESTAMP' } + end + add_index :dmsf_workflow_step_assignments, + %i[dmsf_workflow_step_id dmsf_file_revision_id], + # The default index name exceeds the index name limit + name: :index_dmsf_wrkfl_step_assigns_on_wrkfl_step_id_and_frev_id, + unique: true + end +end diff --git a/db/migrate/20120822100404_create_dmsf_workflow_step_actions.rb b/db/migrate/20120822100404_create_dmsf_workflow_step_actions.rb new file mode 100644 index 00000000..cf2bdb01 --- /dev/null +++ b/db/migrate/20120822100404_create_dmsf_workflow_step_actions.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Create table +class CreateDmsfWorkflowStepActions < ActiveRecord::Migration[4.2] + def change + create_table :dmsf_workflow_step_actions do |t| + t.references :dmsf_workflow_step_assignment, null: false + t.integer :action, null: false + t.text :note + t.datetime :created_at, default: -> { 'CURRENT_TIMESTAMP' } + t.integer :author_id, null: false + end + add_index :dmsf_workflow_step_actions, + :dmsf_workflow_step_assignment_id, + # The default index name exceeds the index name limit + name: :idx_dmsf_wfstepact_on_wfstepassign_id + end +end diff --git a/db/migrate/20130819013955_update_projects.rb b/db/migrate/20130819013955_update_projects.rb new file mode 100644 index 00000000..ddab838d --- /dev/null +++ b/db/migrate/20130819013955_update_projects.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class UpdateProjects < ActiveRecord::Migration[4.2] + def change + # DMSF - project's root folder notification + add_column :projects, :dmsf_notification, :boolean, null: false, default: false + end +end diff --git a/db/migrate/20131108141401_add_index_to_dmsf_files.rb b/db/migrate/20131108141401_add_index_to_dmsf_files.rb new file mode 100644 index 00000000..fb21f8bf --- /dev/null +++ b/db/migrate/20131108141401_add_index_to_dmsf_files.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add index +class AddIndexToDmsfFiles < ActiveRecord::Migration[4.2] + def change + add_index :dmsf_files, :project_id + end +end diff --git a/db/migrate/20131108141402_add_index_to_dmsf_folders.rb b/db/migrate/20131108141402_add_index_to_dmsf_folders.rb new file mode 100644 index 00000000..b1b7c3bc --- /dev/null +++ b/db/migrate/20131108141402_add_index_to_dmsf_folders.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add index +class AddIndexToDmsfFolders < ActiveRecord::Migration[4.2] + def change + add_index :dmsf_folders, :project_id + end +end diff --git a/db/migrate/20131113141401_add_index_to_dmsf_file_revision.rb b/db/migrate/20131113141401_add_index_to_dmsf_file_revision.rb new file mode 100644 index 00000000..96d0aca3 --- /dev/null +++ b/db/migrate/20131113141401_add_index_to_dmsf_file_revision.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add index +class AddIndexToDmsfFileRevision < ActiveRecord::Migration[4.2] + def change + add_index :dmsf_file_revisions, :dmsf_file_id + end +end diff --git a/db/migrate/20131113141402_add_index_to_dmsf_lock.rb b/db/migrate/20131113141402_add_index_to_dmsf_lock.rb new file mode 100644 index 00000000..f5b9c8db --- /dev/null +++ b/db/migrate/20131113141402_add_index_to_dmsf_lock.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add index +class AddIndexToDmsfLock < ActiveRecord::Migration[4.2] + def change + add_index :dmsf_locks, :entity_id + end +end diff --git a/db/migrate/20131113141403_create_dmsf_links.rb b/db/migrate/20131113141403_create_dmsf_links.rb new file mode 100644 index 00000000..e648b698 --- /dev/null +++ b/db/migrate/20131113141403_create_dmsf_links.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Create table +class CreateDmsfLinks < ActiveRecord::Migration[4.2] + def change + create_table :dmsf_links do |t| + t.integer :target_project_id, null: false + t.integer :target_id, null: false + t.string :target_type, limit: 10, null: false + t.string :name, null: false + t.references :project, null: false + t.references :dmsf_folder + t.boolean :deleted, default: false, null: false + t.integer :deleted_by_user_id + t.timestamps null: false + end + add_index :dmsf_links, :project_id + end +end diff --git a/db/migrate/20140314132501_notifications_on.rb b/db/migrate/20140314132501_notifications_on.rb new file mode 100644 index 00000000..2a1e6186 --- /dev/null +++ b/db/migrate/20140314132501_notifications_on.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modify columns +class NotificationsOn < ActiveRecord::Migration[4.2] + def up + # Switch on the default notifications for new projects and folders + change_column :projects, :dmsf_notification, :boolean, default: true, null: true + change_column :dmsf_folders, :notification, :boolean, default: true, null: false + end + + def down + change_column :projects, :dmsf_notification, :boolean, default: false, null: true + change_column :dmsf_folders, :notification, :boolean, default: false + end +end diff --git a/db/migrate/20140519133201_trash_bin.rb b/db/migrate/20140519133201_trash_bin.rb new file mode 100644 index 00000000..c5708d36 --- /dev/null +++ b/db/migrate/20140519133201_trash_bin.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add columns +class TrashBin < ActiveRecord::Migration[4.2] + def up + # DMSF - project's root folder notification + change_table :dmsf_folders, bulk: true do |t| + t.column :deleted, :boolean, default: false, null: false + t.column :deleted_by_user_id, :integer + end + DmsfFolder.reset_column_information + DmsfFolder.update_all deleted: false + end + + def down + change_table :dmsf_folders, bulk: true do |t| + t.remove :deleted + t.remove :deleted_by_user_id + end + end +end diff --git a/db/migrate/20141013102501_remove_project_from_revision.rb b/db/migrate/20141013102501_remove_project_from_revision.rb new file mode 100644 index 00000000..ce676c42 --- /dev/null +++ b/db/migrate/20141013102501_remove_project_from_revision.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# remove column +class RemoveProjectFromRevision < ActiveRecord::Migration[4.2] + def up + remove_column :dmsf_file_revisions, :project_id + end + + def down + add_column :dmsf_file_revisions, :project_id, :integer, null: true + DmsfFileRevision.reset_column_information + DmsfFileRevision.find_each do |revision| + if revision.dmsf_file + revision.project_id = revision.dmsf_file.project.id + revision.save! + end + end + end +end diff --git a/db/migrate/20141015132701_remove_folder_from_revision.rb b/db/migrate/20141015132701_remove_folder_from_revision.rb new file mode 100644 index 00000000..6837a765 --- /dev/null +++ b/db/migrate/20141015132701_remove_folder_from_revision.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Remove column +class RemoveFolderFromRevision < ActiveRecord::Migration[4.2] + def up + remove_column :dmsf_file_revisions, :dmsf_folder_id + end + + def down + add_column :dmsf_file_revisions, :dmsf_folder_id, :integer, null: true + DmsfFileRevision.reset_column_information + DmsfFileRevision.find_each do |revision| + if revision.dmsf_file + revision.dmsf_folder_id = revision.dmsf_file.dmsf_folder_id + revision.save validate: false + end + end + end +end diff --git a/db/migrate/20141205143001_remove_uniqueness_from_wf.rb b/db/migrate/20141205143001_remove_uniqueness_from_wf.rb new file mode 100644 index 00000000..2b37b45a --- /dev/null +++ b/db/migrate/20141205143001_remove_uniqueness_from_wf.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Remove index +class RemoveUniquenessFromWf < ActiveRecord::Migration[4.2] + def up + remove_index(:dmsf_workflows, :name) if index_exists?(:dmsf_workflows, :name) + end +end diff --git a/db/migrate/20150120152101_notifications_nullable.rb b/db/migrate/20150120152101_notifications_nullable.rb new file mode 100644 index 00000000..9f65e2b8 --- /dev/null +++ b/db/migrate/20150120152101_notifications_nullable.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modify columns +class NotificationsNullable < ActiveRecord::Migration[4.2] + def up + change_column :projects, :dmsf_notification, :boolean, default: false, null: true + change_column :dmsf_folders, :notification, :boolean, default: false, null: true + change_column :dmsf_files, :notification, :boolean, default: false, null: true + end +end diff --git a/db/migrate/20150130052716_add_external.rb b/db/migrate/20150130052716_add_external.rb new file mode 100644 index 00000000..9c3fa968 --- /dev/null +++ b/db/migrate/20150130052716_add_external.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class AddExternal < ActiveRecord::Migration[4.2] + def up + change_table :dmsf_links, bulk: true do |t| + t.change :target_id, :integer, null: true + t.column :external_url, :string, null: true + end + end + + def down + remove_column :dmsf_links, :external_url + end +end diff --git a/db/migrate/20150202010301_add_user_to_links.rb b/db/migrate/20150202010301_add_user_to_links.rb new file mode 100644 index 00000000..c13d3c21 --- /dev/null +++ b/db/migrate/20150202010301_add_user_to_links.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class AddUserToLinks < ActiveRecord::Migration[4.2] + def change + add_column :dmsf_links, :user_id, :integer + end +end diff --git a/db/migrate/20150910153701_title_not_null.rb b/db/migrate/20150910153701_title_not_null.rb new file mode 100644 index 00000000..efbad5ee --- /dev/null +++ b/db/migrate/20150910153701_title_not_null.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modify column +class TitleNotNull < ActiveRecord::Migration[4.2] + def up + change_column :dmsf_file_revisions, :title, :string, null: false + end + + def down + change_column :dmsf_file_revisions, :title, :string, null: true + end +end diff --git a/db/migrate/20151020141801_large_files.rb b/db/migrate/20151020141801_large_files.rb new file mode 100644 index 00000000..6d1cad2e --- /dev/null +++ b/db/migrate/20151020141801_large_files.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modify column +class LargeFiles < ActiveRecord::Migration[4.2] + def up + change_column :dmsf_file_revisions, :size, :bigint, null: true + end + + def down + change_column :dmsf_file_revisions, :size, :int, null: true + end +end diff --git a/db/migrate/20151209100001_title_format.rb b/db/migrate/20151209100001_title_format.rb new file mode 100644 index 00000000..dd83446d --- /dev/null +++ b/db/migrate/20151209100001_title_format.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class TitleFormat < ActiveRecord::Migration[4.2] + def change + add_column :members, :title_format, :text, null: true, limit: 100 + end +end diff --git a/db/migrate/20160215125801_approval_workflow_status.rb b/db/migrate/20160215125801_approval_workflow_status.rb new file mode 100644 index 00000000..a648224c --- /dev/null +++ b/db/migrate/20160215125801_approval_workflow_status.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class ApprovalWorkflowStatus < ActiveRecord::Migration[4.2] + def up + add_column :dmsf_workflows, :status, :integer, + null: false, default: DmsfWorkflow::STATUS_ACTIVE + DmsfWorkflow.reset_column_information + DmsfWorkflow.find_each { |wf| wf.update_attribute(:status, DmsfWorkflow::STATUS_ACTIVE) } + end + + def down + remove_column :dmsf_workflows, :status + end +end diff --git a/db/migrate/20160217133001_status_deleted.rb b/db/migrate/20160217133001_status_deleted.rb new file mode 100644 index 00000000..48a913ff --- /dev/null +++ b/db/migrate/20160217133001_status_deleted.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modisy columns +class StatusDeleted < ActiveRecord::Migration[4.2] + def up + case ActiveRecord::Base.connection.adapter_name.downcase + when /postgresql/ + execute 'ALTER TABLE dmsf_folders ALTER deleted DROP DEFAULT;' + change_column :dmsf_folders, :deleted, + 'INTEGER USING CASE deleted WHEN TRUE THEN 1 ELSE 0 END', + null: false, default: DmsfFolder::STATUS_ACTIVE + execute 'ALTER TABLE dmsf_files ALTER deleted DROP DEFAULT;' + change_column :dmsf_files, :deleted, 'INTEGER USING CASE deleted WHEN TRUE THEN 1 ELSE 0 END', + null: false, default: DmsfFile::STATUS_ACTIVE + execute 'ALTER TABLE dmsf_file_revisions ALTER deleted DROP DEFAULT;' + change_column :dmsf_file_revisions, :deleted, + 'INTEGER USING CASE deleted WHEN TRUE THEN 1 ELSE 0 END', + null: false, default: DmsfFileRevision::STATUS_ACTIVE + execute 'ALTER TABLE dmsf_links ALTER deleted DROP DEFAULT;' + change_column :dmsf_links, :deleted, + 'INTEGER USING CASE deleted WHEN TRUE THEN 1 ELSE 0 END', + null: false, default: DmsfLink::STATUS_ACTIVE + else + change_column :dmsf_folders, :deleted, :integer, default: DmsfFolder::STATUS_ACTIVE + change_column :dmsf_files, :deleted, :integer, default: DmsfFile::STATUS_ACTIVE + change_column :dmsf_file_revisions, :deleted, :integer, default: DmsfFile::STATUS_ACTIVE + change_column :dmsf_links, :deleted, :integer, default: DmsfFile::STATUS_ACTIVE + end + end + + def down + case ActiveRecord::Base.connection.adapter_name.downcase + when /postgresql/ + execute 'ALTER TABLE dmsf_folders ALTER deleted DROP DEFAULT;' + change_column :dmsf_folders, :deleted, + 'BOOLEAN USING CASE WHEN deleted=1 THEN TRUE ELSE FALSE END', + null: false, default: false + execute 'ALTER TABLE dmsf_files ALTER deleted DROP DEFAULT;' + change_column :dmsf_files, :deleted, + 'BOOLEAN USING CASE WHEN deleted=1 THEN TRUE ELSE FALSE END', + null: false, default: false + execute 'ALTER TABLE dmsf_file_revisions ALTER deleted DROP DEFAULT;' + change_column :dmsf_file_revisions, :deleted, + 'BOOLEAN USING CASE WHEN deleted=1 THEN TRUE ELSE FALSE END', + null: false, default: false + execute 'ALTER TABLE dmsf_links ALTER deleted DROP DEFAULT;' + change_column :dmsf_links, :deleted, + 'BOOLEAN USING CASE WHEN deleted=1 THEN TRUE ELSE FALSE END', + null: false, default: false + else + change_column :dmsf_folders, :deleted, :boolean, null: false, default: false + change_column :dmsf_files, :deleted, :boolean, null: false, default: false + change_column :dmsf_file_revisions, :deleted, :boolean, + null: false, default: false + change_column :dmsf_links, :deleted, :boolean, null: false, default: false + end + end +end diff --git a/db/migrate/20160222140401_approval_workflow_std_fields.rb b/db/migrate/20160222140401_approval_workflow_std_fields.rb new file mode 100644 index 00000000..5de7ef82 --- /dev/null +++ b/db/migrate/20160222140401_approval_workflow_std_fields.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add columns +class ApprovalWorkflowStdFields < ActiveRecord::Migration[4.2] + def up + change_table :dmsf_workflows, bulk: true do |t| + t.column :updated_on, :timestamp + t.column :created_on, :datetime + t.column :author_id, :integer + end + DmsfWorkflow.reset_column_information + # Set updated_on + DmsfWorkflow.find_each(&:touch) + # Set created_on and author_id + admin_ids = User.active.where(admin: true).limit(1).ids + DmsfWorkflow.update_all ['created_on = updated_on, author_id = ?', admin_ids.first] + end + + def down + change_table :dmsf_workflows, bulk: true do |t| + t.remove :updated_on + t.remove :created_on + t.remove :author_id + end + end +end diff --git a/db/migrate/20160421150501_add_digest_to_revision.rb b/db/migrate/20160421150501_add_digest_to_revision.rb new file mode 100644 index 00000000..e309720a --- /dev/null +++ b/db/migrate/20160421150501_add_digest_to_revision.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class AddDigestToRevision < ActiveRecord::Migration[4.2] + def up + add_column :dmsf_file_revisions, :digest, :string, limit: 40, default: '', null: false + end +end diff --git a/db/migrate/20161223133200_create_dmsf_public_urls.rb b/db/migrate/20161223133200_create_dmsf_public_urls.rb new file mode 100644 index 00000000..1027a551 --- /dev/null +++ b/db/migrate/20161223133200_create_dmsf_public_urls.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Create table +class CreateDmsfPublicUrls < ActiveRecord::Migration[4.2] + def change + create_table :dmsf_public_urls do |t| + t.string :token, null: false, limit: 32 + t.references :dmsf_file, null: false + t.references :user, null: false + t.datetime :expire_at, null: false + t.timestamps + end + add_index :dmsf_public_urls, :token + end +end diff --git a/db/migrate/20170103164701_add_name_to_appoval_workflow_step.rb b/db/migrate/20170103164701_add_name_to_appoval_workflow_step.rb new file mode 100644 index 00000000..f408cc40 --- /dev/null +++ b/db/migrate/20170103164701_add_name_to_appoval_workflow_step.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class AddNameToAppovalWorkflowStep < ActiveRecord::Migration[4.2] + def change + add_column :dmsf_workflow_steps, :name, :string, limit: 30, null: true + end +end diff --git a/db/migrate/20170118142001_dmsf_file_container.rb b/db/migrate/20170118142001_dmsf_file_container.rb new file mode 100644 index 00000000..e1a0f046 --- /dev/null +++ b/db/migrate/20170118142001_dmsf_file_container.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class DmsfFileContainer < ActiveRecord::Migration[4.2] + def up + change_table :dmsf_files, bulk: true do |t| + t.remove_index :project_id + t.rename :project_id, :container_id + t.column :container_type, :string, limit: 30, null: false, default: 'Project' + t.index %i[container_id container_type] + end + DmsfFile.update_all container_type: 'Project' + end + + def down + change_table :dmsf_files, bulk: true do |t| + t.remove_index %i[container_id container_type] + t.remove :container_type + t.rename :container_id, :project_id + t.index :project_id + end + end +end diff --git a/db/migrate/20170204214753_add_revision_to_dmsf_lock.rb b/db/migrate/20170204214753_add_revision_to_dmsf_lock.rb new file mode 100644 index 00000000..bbcb17bc --- /dev/null +++ b/db/migrate/20170204214753_add_revision_to_dmsf_lock.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman , carlolars +# +# 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 +# . + +# Add column +class AddRevisionToDmsfLock < ActiveRecord::Migration[4.2] + def change + add_column :dmsf_locks, :revision, :integer, null: true + end +end diff --git a/db/migrate/20170214153223_add_dmsf_file_last_revision_id_to_dmsf_lock.rb b/db/migrate/20170214153223_add_dmsf_file_last_revision_id_to_dmsf_lock.rb new file mode 100644 index 00000000..e8d5ffcd --- /dev/null +++ b/db/migrate/20170214153223_add_dmsf_file_last_revision_id_to_dmsf_lock.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman , carlolars +# +# 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 +# . + +# Rename column +class AddDmsfFileLastRevisionIdToDmsfLock < ActiveRecord::Migration[4.2] + def up + rename_column :dmsf_locks, :revision, :dmsf_file_last_revision_id + end + + def down + rename_column :dmsf_locks, :dmsf_file_last_revision_id, :revision + end +end diff --git a/db/migrate/20170217141601_add_dmsf_not_inheritable_to_custom_fields.rb b/db/migrate/20170217141601_add_dmsf_not_inheritable_to_custom_fields.rb new file mode 100644 index 00000000..ab03fb89 --- /dev/null +++ b/db/migrate/20170217141601_add_dmsf_not_inheritable_to_custom_fields.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class AddDmsfNotInheritableToCustomFields < ActiveRecord::Migration[4.2] + def change + add_column :custom_fields, :dmsf_not_inheritable, :boolean, + null: false, default: false + end +end diff --git a/db/migrate/20170323131231_dmsf_description_limit.rb b/db/migrate/20170323131231_dmsf_description_limit.rb new file mode 100644 index 00000000..9da70eb9 --- /dev/null +++ b/db/migrate/20170323131231_dmsf_description_limit.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# modify column +class DmsfDescriptionLimit < ActiveRecord::Migration[4.2] + def up + change_column :projects, :dmsf_description, :text, null: true, limit: 65_535 + change_column :dmsf_folders, :description, :text, null: true, limit: 65_535 + end + + def down + change_column :projects, :dmsf_description, :text + change_column :dmsf_folders, :description, :text + end +end diff --git a/db/migrate/20170330131901_create_dmsf_folder_permissions.rb b/db/migrate/20170330131901_create_dmsf_folder_permissions.rb new file mode 100644 index 00000000..6c7dfc54 --- /dev/null +++ b/db/migrate/20170330131901_create_dmsf_folder_permissions.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Create table +class CreateDmsfFolderPermissions < ActiveRecord::Migration[4.2] + def change + create_table :dmsf_folder_permissions do |t| + t.references :dmsf_folder + t.integer :object_id, null: false + t.string :object_type, limit: 30, null: false + t.datetime :created_at, default: -> { 'CURRENT_TIMESTAMP' } + end + add_index :dmsf_folder_permissions, :dmsf_folder_id + end +end diff --git a/db/migrate/20170421101901_dmsf_file_container_rollback.rb b/db/migrate/20170421101901_dmsf_file_container_rollback.rb new file mode 100644 index 00000000..617710a3 --- /dev/null +++ b/db/migrate/20170421101901_dmsf_file_container_rollback.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Rollback container +class DmsfFileContainerRollback < ActiveRecord::Migration[4.2] + def up + # Add system folder_flag to dmsf_folders + add_column :dmsf_folders, :system, :boolean, null: false, default: false + # Create necessary folders + new_folder_ids = [] + description = 'Documents assigned to issues' + DmsfFile.where(container_type: 'Issue').find_each do |file| + issue = Issue.find_by(id: file.container_id) + unless issue + Rails.logger.error "Issue ##{file.container_id} not found" + next + end + # Parent folder + parent = DmsfFolder.where(project_id: issue.project.id, title: '.Issues', description: description).first + unless parent + parent = DmsfFolder.new + parent.project_id = issue.project.id + parent.title = '.Issues' + parent.description = description + parent.user_id = User.anonymous.id + parent.save! + new_folder_ids << parent.id + end + # Issue folder + title = "#{issue.id} - #{DmsfFolder.get_valid_title(issue.subject)}" + folder = DmsfFolder.find_by(project_id: issue.project.id, dmsf_folder_id: parent.id, title: title) + unless folder + folder = DmsfFolder.new + folder.project_id = issue.project.id + folder.dmsf_folder_id = parent.id + folder.title = "#{issue.id} - #{DmsfFolder.get_valid_title(issue.subject)}" + folder.user_id = User.anonymous.id + folder.save! + end + new_folder_ids << folder.id + # Move the file into the new folder + file.dmsf_folder_id = folder.id + # The save methos requires project_id + class << file + attr_accessor :project_id + end + file.save(validate: false) + end + # Make DB changes in dmsf_files + change_table :dmsf_files, bulk: true do |t| + t.remove_index %i[container_id container_type] + # TODO: The column cannot be removed on SQL server due to NOT NULL constraint. + # The constraint's name is random and therefore cannot be easily removed. + t.remove :container_type if ActiveRecord::Base.connection.adapter_name.downcase != 'sqlserver' + t.rename :container_id, :project_id + t.index :project_id + end + # Initialize system folder_flag to dmsf_folders + DmsfFolder.where(id: new_folder_ids).update_all system: true + end + + def down + # dmsf_files + file_folder_ids = DmsfFile.joins(:dmsf_folder).where(dmsf_folders: { system: true }).pluck( + 'dmsf_files.id, dmsf_folders.title' + ) + change_table :dmsf_files, bulk: true do |t| + t.remove_index(:project_id) if t.index_exists?(:project_id) + t.rename :project_id, :container_id + # Temporarily added for the save method + t.column :project_id, :int, null: true + t.column :container_type, :string, limit: 30, null: false, default: 'Project' + end + DmsfFile.update_all container_type: 'Project' + file_folder_ids.each do |id, title| + file = DmsfFile.find_by(id: id) + next unless file && (title =~ /(^\d+) - .*/) + + file.container_id = Regexp.last_match(1).to_i + file.container_type = 'Issue' + file.save! + end + change_table :dmsf_files, bulk: true do |t| + t.remove :project_id # temporarily added for the save method + t.index(%i[container_id container_type]) unless t.index_exists?(%i[container_id container_type]) + end + # dmsf_folders + DmsfFolder.where(system: true).delete_all + remove_column :dmsf_folders, :system + end +end diff --git a/db/migrate/20170422104901_migrate_documents.rb b/db/migrate/20170422104901_migrate_documents.rb new file mode 100644 index 00000000..9d5d4d3c --- /dev/null +++ b/db/migrate/20170422104901_migrate_documents.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Migrate documents in the file system +class MigrateDocuments < ActiveRecord::Migration[4.2] + def up + # Migrate all documents from dmsf/p_{project identifier} to dmsf/{year}/{month} + DmsfFileRevision.find_each do |dmsf_file_revision| + if dmsf_file_revision.dmsf_file + if dmsf_file_revision.dmsf_file.project + origin = disk_file(dmsf_file_revision) + if origin + if File.exist?(origin) + target = dmsf_file_revision.disk_file(search_if_not_exists: false) + if target + if File.exist?(target) + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: Target '#{target}' exists" + say msg + Rails.logger.error msg + else + begin + FileUtils.mv origin, target, verbose: true + folder = storage_base_path(dmsf_file_revision) + Dir.rmdir(folder) if folder && Dir.empty?(folder) + rescue StandardError => e + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: #{e.message}" + say msg + Rails.logger.error msg + end + end + else + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: target = nil" + say msg + Rails.logger.error msg + end + else + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: Origin '#{origin}' doesn't exist" + say msg + Rails.logger.error msg + end + else + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: disk_file = nil" + say msg + Rails.logger.error msg + end + else + msg = "DmsfFile ID #{dmsf_file_revision.dmsf_file.id}: project = nil" + say msg + Rails.logger.error msg + end + else + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: dmsf_file = nil" + say msg + Rails.logger.error msg + end + end + end + + def down + # Migrate all documents from dmsf/{year}/{month} to dmsf/p_{project identifier} + DmsfFileRevision.find_each do |dmsf_file_revision| + if dmsf_file_revision.dmsf_file + if dmsf_file_revision.dmsf_file.project + origin = dmsf_file_revision.disk_file(search_if_not_exists: false) + if origin + if File.exist?(origin) + target = disk_file(dmsf_file_revision) + if target + if File.exist?(target) + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: Target '#{target}' exists" + say msg + Rails.logger.error msg + else + begin + FileUtils.mv origin, target, verbose: true + folder = dmsf_file_revision.storage_base_path + Dir.rmdir(folder) if folder && Dir.empty?(folder) + rescue StandardError => e + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: #{e.message}" + say msg + Rails.logger.error msg + end + end + else + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: target = nil" + say msg + Rails.logger.error msg + end + else + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: Origin '#{origin}' doesn't exist" + say msg + Rails.logger.error msg + end + else + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: disk_file = nil" + say msg + Rails.logger.error msg + end + else + msg = "DmsfFile ID #{dmsf_file_revision.dmsf_file.id}: project = nil" + say msg + Rails.logger.error msg + end + else + msg = "DmsfFileRevisions ID #{dmsf_file_revision.id}: dmsf_file = nil" + say msg + Rails.logger.error msg + end + end + end + + def storage_base_path(dmsf_file_revision) + return nil unless dmsf_file_revision&.dmsf_file&.project + + project_base = dmsf_file_revision.dmsf_file.project.identifier.gsub(/[^\w.\-]/, '_') + "#{DmsfFile.storage_path}/p_#{project_base}" + end + + def disk_file(dmsf_file_revision) + path = storage_base_path(dmsf_file_revision) + if path + FileUtils.mkdir_p(path) + return "#{path}/#{dmsf_file_revision.disk_filename}" + end + nil + end +end diff --git a/db/migrate/20170526144701_dmsf_attachable.rb b/db/migrate/20170526144701_dmsf_attachable.rb new file mode 100644 index 00000000..ed721f79 --- /dev/null +++ b/db/migrate/20170526144701_dmsf_attachable.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class DmsfAttachable < ActiveRecord::Migration[4.2] + def up + # DMSF - project's root folder notification + add_column :projects, :dmsf_act_as_attachable, :integer, default: 1, null: false + Project.update_all dmsf_act_as_attachable: 1 + end + + def down + remove_column :projects, :dmsf_act_as_attachable + end +end diff --git a/db/migrate/20171027124101_change_revision_digest_limit_to_64.rb b/db/migrate/20171027124101_change_revision_digest_limit_to_64.rb new file mode 100644 index 00000000..4e4104c0 --- /dev/null +++ b/db/migrate/20171027124101_change_revision_digest_limit_to_64.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modify column +class ChangeRevisionDigestLimitTo64 < ActiveRecord::Migration[4.2] + def up + change_column :dmsf_file_revisions, :digest, :string, limit: 64 + end + + def down + # Mysql2::Error: Data too long for column 'digest' + # Recalculation of checksums for all revisions is technically possible but costs are to high. + # change_column :dmsf_file_revisions, :digest, :string, limit: 40 + end +end diff --git a/db/migrate/20171110155901_add_index_to_dmsf_folder.rb b/db/migrate/20171110155901_add_index_to_dmsf_folder.rb new file mode 100644 index 00000000..aa69b39e --- /dev/null +++ b/db/migrate/20171110155901_add_index_to_dmsf_folder.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add index +class AddIndexToDmsfFolder < ActiveRecord::Migration[4.2] + def change + add_index :dmsf_folders, :dmsf_folder_id + end +end diff --git a/db/migrate/20180216152501_rename_title_format.rb b/db/migrate/20180216152501_rename_title_format.rb new file mode 100644 index 00000000..db949379 --- /dev/null +++ b/db/migrate/20180216152501_rename_title_format.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modify column +class RenameTitleFormat < ActiveRecord::Migration[4.2] + def up + rename_column :members, :title_format, :dmsf_title_format + end + + def down + rename_column :members, :dmsf_title_format, :title_format + end +end diff --git a/db/migrate/20180903132101_fast_links.rb b/db/migrate/20180903132101_fast_links.rb new file mode 100644 index 00000000..f488b179 --- /dev/null +++ b/db/migrate/20180903132101_fast_links.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class FastLinks < ActiveRecord::Migration[4.2] + def change + add_column :members, :dmsf_fast_links, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20192703123101_workflow_started_by.rb b/db/migrate/20192703123101_workflow_started_by.rb new file mode 100644 index 00000000..22309adb --- /dev/null +++ b/db/migrate/20192703123101_workflow_started_by.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modify columns +class WorkflowStartedBy < ActiveRecord::Migration[5.2] + def change + change_table :dmsf_file_revisions, bulk: true do |t| + t.rename :dmsf_workflow_assigned_by, :dmsf_workflow_assigned_by_user_id + t.rename :dmsf_workflow_started_by, :dmsf_workflow_started_by_user_id + end + end +end diff --git a/db/migrate/20200218142414_add_index_on_dmsf_file_revision_id_to_access.rb b/db/migrate/20200218142414_add_index_on_dmsf_file_revision_id_to_access.rb new file mode 100644 index 00000000..51132bd5 --- /dev/null +++ b/db/migrate/20200218142414_add_index_on_dmsf_file_revision_id_to_access.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add index +class AddIndexOnDmsfFileRevisionIdToAccess < ActiveRecord::Migration[5.2] + def change + add_index :dmsf_file_revision_accesses, :dmsf_file_revision_id + end +end diff --git a/db/migrate/20200423071301_add_indexes_on_dmsf_folder_id.rb b/db/migrate/20200423071301_add_indexes_on_dmsf_folder_id.rb new file mode 100644 index 00000000..93f46851 --- /dev/null +++ b/db/migrate/20200423071301_add_indexes_on_dmsf_folder_id.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add indexes +class AddIndexesOnDmsfFolderId < ActiveRecord::Migration[5.2] + def change + add_index :dmsf_files, :dmsf_folder_id + add_index :dmsf_links, :dmsf_folder_id + end +end diff --git a/db/migrate/20200813075501_change_index_in_dmsf_locks.rb b/db/migrate/20200813075501_change_index_in_dmsf_locks.rb new file mode 100644 index 00000000..5a3d4db6 --- /dev/null +++ b/db/migrate/20200813075501_change_index_in_dmsf_locks.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modify index +class ChangeIndexInDmsfLocks < ActiveRecord::Migration[5.2] + def up + change_table :dmsf_locks, bulk: true do |t| + t.remove_index :entity_id + t.index %i[entity_id entity_type] + end + end + + def down + change_table :dmsf_locks, bulk: true do |t| + t.remove_index %i[entity_id entity_type] + t.index :entity_id + end + end +end diff --git a/db/migrate/20210115120901_add_owner_to_dmsf_lock.rb b/db/migrate/20210115120901_add_owner_to_dmsf_lock.rb new file mode 100644 index 00000000..a6817f19 --- /dev/null +++ b/db/migrate/20210115120901_add_owner_to_dmsf_lock.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class AddOwnerToDmsfLock < ActiveRecord::Migration[5.2] + def change + add_column :dmsf_locks, :owner, :string, null: true + end +end diff --git a/db/migrate/20220317100901_add_patch_version.rb b/db/migrate/20220317100901_add_patch_version.rb new file mode 100644 index 00000000..ec233f0f --- /dev/null +++ b/db/migrate/20220317100901_add_patch_version.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class AddPatchVersion < ActiveRecord::Migration[5.2] + def change + add_column :dmsf_file_revisions, :patch_version, :integer, + null: true, after: :minor_version + end +end diff --git a/db/migrate/20220906151100_add_projects_default_dmsf_query_id.rb b/db/migrate/20220906151100_add_projects_default_dmsf_query_id.rb new file mode 100644 index 00000000..df59e71a --- /dev/null +++ b/db/migrate/20220906151100_add_projects_default_dmsf_query_id.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add column +class AddProjectsDefaultDmsfQueryId < ActiveRecord::Migration[4.2] + def change + add_column :projects, :default_dmsf_query_id, :integer, default: nil + end +end diff --git a/db/migrate/20230105082201_null_minor_version.rb b/db/migrate/20230105082201_null_minor_version.rb new file mode 100644 index 00000000..5f69757b --- /dev/null +++ b/db/migrate/20230105082201_null_minor_version.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Modify column +class NullMinorVersion < ActiveRecord::Migration[4.2] + def up + change_column_null :dmsf_file_revisions, :minor_version, true + end + + def down + change_column_null :dmsf_file_revisions, :minor_version, false, 0 + end +end diff --git a/db/migrate/20230426130301_add_uniqness_to_step_assignments.rb b/db/migrate/20230426130301_add_uniqness_to_step_assignments.rb new file mode 100644 index 00000000..b2e6ac97 --- /dev/null +++ b/db/migrate/20230426130301_add_uniqness_to_step_assignments.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Add indexes +class AddUniqnessToStepAssignments < ActiveRecord::Migration[5.2] + def change + add_index :dmsf_workflow_step_assignments, %i[dmsf_workflow_step_id dmsf_file_revision_id], + unique: true, + name: 'index_workflow_step_assignments_on_step_id_and_revision_id' + end +end diff --git a/db/migrate/20240829093801_rename_dmsf_digest_token.rb b/db/migrate/20240829093801_rename_dmsf_digest_token.rb new file mode 100644 index 00000000..7a11844b --- /dev/null +++ b/db/migrate/20240829093801_rename_dmsf_digest_token.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Rename DMSF digest token +class RenameDmsfDigestToken < ActiveRecord::Migration[6.1] + def up + Token.where(action: 'dmsf-webdav-digest').update_all action: 'dmsf_webdav_digest' + end + + def down + Token.where(action: 'dmsf_webdav_digest').update_all action: 'dmsf-webdav-digest' + end +end diff --git a/extra/api/api_client.rb b/extra/api/api_client.rb new file mode 100644 index 00000000..d5404cce --- /dev/null +++ b/extra/api/api_client.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require 'rubygems' +require 'active_resource' + +# Simple REST API client in Ruby +# usage: ruby api_client.rb [login] [password] + +# Dmsf file +class DmsfFile < ActiveResource::Base + self.site = 'https://localhost:3000/' + self.user = ARGV[0] + self.password = ARGV[1] +end + +# 2. Get a document +FILE_ID = 41_532 +file = DmsfFile.find FILE_ID +if file + puts file.id + puts file.title + puts file.name + puts file.version + puts file.project_id + puts file.content_url +else + puts "No file with id = #{FILE_ID} found" +end diff --git a/extra/api/api_client.sh b/extra/api/api_client.sh new file mode 100644 index 00000000..2794adc0 --- /dev/null +++ b/extra/api/api_client.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Authentication as input parameters either as login + password or the API key +#USER_LOGIN="${1}" +#USER_PASSWORD="${2}" +USER_API_KEY="${1}" + +# BOTH XML and JSON formats are supported. +# Just replace .xml with .json +# +# Uncomment a corresponding line to the case you would like to test + +# 1. List of documents in a given folder or the root folder +#curl -v -H "Content-Type: application/xml" -X GET -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/projects/12/dmsf.xml +#curl -v -H "Content-Type: application/xml" -X GET -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/projects/12/dmsf.xml?folder_id=5155 +#curl -v -H "Content-Type: application/xml" -X GET -u ${USER_LOGIN}:${USER_PASSWORD} "http://localhost:3000/projects/12/dmsf.xml?limit=2&offset=1" + +# 2. Get a document +#curl -v -H "Content-Type: application/xml" -X GET -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/dmsf/files/17216.xml +#curl -v -H "Content-Type: application/octet-stream" -X GET -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/dmsf/files/41532/download > file.txt + +# 3. Upload a document into a given folder or the root folder +#curl --data-binary "@cat.gif" -H "Content-Type: application/octet-stream" -X POST -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/projects/12/dmsf/upload.xml?filename=cat.gif +#curl -v -H "Content-Type: application/xml" -X POST --data "@file.xml" -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/projects/12/dmsf/commit.xml + +# 4. Create a new revision +#curl -v -H "Content-Type: application/xml" -X POST --data "@revision.xml" -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/dmsf/files/232565/revision/create.xml + +# 5. Entries operation +# 5.1 Copy document(s)/folder(s) +#curl -v -H "Content-Type: application/xml" -X POST --data "@entries.xml" -H "X-Redmine-API-Key: ${USER_API_KEY}" "http://localhost:3000/projects/3342/dmsf/entries.xml?ids[]=file-254566©_entries=true" +# 5.2 Move document(s)/folder(s) +#curl -v -H "Content-Type: application/xml" -X POST --data "@entries.xml" -H "X-Redmine-API-Key: ${USER_API_KEY}" "http://localhost:3000/projects/3342/dmsf/entries.xml?ids[]=file-254566&move_entries=true" +# 5.3 Download document(x)/folders(s) +#curl -v -H "Content-Type: application/octet-stream" -X POST --data "" -H "X-Redmine-API-Key: ${USER_API_KEY}" http://localhost:3000/projects/3342/dmsf/entries.xml?ids[]=file-254566 +# 5.4 Delete document(x)/folder(s) +#curl -v -H "Content-Type: application/xml" -X POST --data "" -H "X-Redmine-API-Key: ${USER_API_KEY}" "http://localhost:3000/projects/3342/dmsf/entries.xml?ids[]=file-254566&delete_entries=true" + +# 6. Delete a document +# a) Move to trash only +# curl -v -H "Content-Type: application/xml" -X DELETE -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/dmsf/files/196118.xml +# b) Delete permanently +# curl -v -H "Content-Type: application/xml" -X DELETE -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/dmsf/files/196118.xml?commit=yes" + +# 7. Create a folder +#curl -v -H "Content-Type: application/xml" -X POST --data "@folder.xml" -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/projects/12/dmsf/create.xml + +# 8. List folder content & check folder existence (by folder title) +# curl -v -H "Content-Type: application/json" -X GET -H "X-Redmine-API-Key: ${USERS_API_KEY}" http://localhost:3000/projects/1/dmsf.json?folder_title=Updated%20title + +# 9. List folder content & check folder existence (by folder id) +# curl -v -H "Content-Type: application/json" -X GET -H "X-Redmine-API-Key: ${USERS_API_KEY}" http://localhost:3000/projects/1/dmsf.json?folder_id=3 +# both returns 404 not found, or json with following structure: +# { +# "dmsf":{ +# "found_folder":{ +# "id":3, +# "title":"Updated title" +# } +# } +#} + +# 10. Update a folder +# curl -v -H "Content-Type: application/json" -X POST --data "@update-folder-payload.json" -H "X-Redmine-API-Key: ${USERS_API_KEY}" http://localhost:3000//projects/#{project_id}/dmsf/save.json?folder_id=#{folder_id} + +# update-folder-payload.json +# { +# "dmsf_folder": { +# "title": title, +# "description": description +# }, +# } + +# 11. Copy a folder +#curl -v -H "Content-Type: application/xml" -X POST --data "@file_or_folder_copy_move.xml" -H "X-Redmine-API-Key: ${USERS_API_KEY}" http://localhost:3000/dmsf/folders/53075/copy/copy.xml + +# 12. Move a folder +#curl -v -H "Content-Type: application/xml" -X POST --data "@file_or_folder_copy_move.xml" -H "X-Redmine-API-Key: ${USERS_API_KEY}" http://localhost:3000/dmsf/folders/53075/copy/move.xml + +# 13. Delete a folder +# a) Move to trash only +# curl -v -H "Content-Type: application/xml" -X DELETE -u ${USER_LOGIN}:${USER_PASSWORD} http://localhost:3000/projects/2387/dmsf/delete.xml?folder_id=#{folder_id} +# b) Delete permanently +# curl -v -H "Content-Type: application/xml" -X DELETE -u ${USER_LOGIN}:${USER_PASSWORD} "http://localhost:3000/projects/2387/dmsf/delete.xml?folder_id=#{folder_id}&commit=yes" + +# 14. Create a symbolic link +# curl -v -H "Content-Type: application/xml" -X POST --data "@link.xml" -H "X-Redmine-API-Key: ${USERS_API_KEY}" http://localhost:3000/dmsf_links.xml \ No newline at end of file diff --git a/extra/api/cat.gif b/extra/api/cat.gif new file mode 100644 index 00000000..0329e659 Binary files /dev/null and b/extra/api/cat.gif differ diff --git a/extra/api/entries.xml b/extra/api/entries.xml new file mode 100644 index 00000000..b7473e52 --- /dev/null +++ b/extra/api/entries.xml @@ -0,0 +1,5 @@ + + + 3342 + 58659 + diff --git a/extra/api/file.xml b/extra/api/file.xml new file mode 100644 index 00000000..399a7b45 --- /dev/null +++ b/extra/api/file.xml @@ -0,0 +1,16 @@ + + + 6118 + + cat.gif + cat + + REST API + From API + A + 1 + 0 + + 15838.c49f68ff81b552d315927df2e27df506 + + diff --git a/extra/api/folder.xml b/extra/api/folder.xml new file mode 100644 index 00000000..262bfe8f --- /dev/null +++ b/extra/api/folder.xml @@ -0,0 +1,7 @@ + + + rest_api + A folder created via REST API + + + diff --git a/extra/api/link.xml b/extra/api/link.xml new file mode 100644 index 00000000..f950050e --- /dev/null +++ b/extra/api/link.xml @@ -0,0 +1,11 @@ + + + 2387 + + link_from + 2387 + + 196119 + + test + \ No newline at end of file diff --git a/extra/api/revision.xml b/extra/api/revision.xml new file mode 100644 index 00000000..fd9bcb7d --- /dev/null +++ b/extra/api/revision.xml @@ -0,0 +1,10 @@ + + + test + test.sql + SQL script + REST API + + Validation + + \ No newline at end of file diff --git a/extra/xapian_indexer.rb b/extra/xapian_indexer.rb new file mode 100644 index 00000000..160c9b65 --- /dev/null +++ b/extra/xapian_indexer.rb @@ -0,0 +1,168 @@ +#!/usr/bin/ruby -W0 + +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Xabier Elkano, Karel Pičman +# +# 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 +# . + +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 diff --git a/init.rb b/init.rb new file mode 100644 index 00000000..a783fb15 --- /dev/null +++ b/init.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +require 'redmine' +require 'zip' +require "#{File.dirname(__FILE__)}/lib/redmine_dmsf" + +Redmine::Plugin.register :redmine_dmsf do + name 'DMSF' + url 'https://www.redmine.org/plugins/redmine_dmsf' + 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' + + requires_redmine version_or_higher: '6.1.0' + + settings partial: 'settings/dmsf_settings', + default: { + 'dmsf_max_file_download' => 0, + 'dmsf_max_email_filesize' => 0, + 'dmsf_storage_directory' => 'files/dmsf', + 'dmsf_index_database' => File.expand_path('dmsf_index', Rails.root), + 'dmsf_stemming_lang' => 'english', + 'dmsf_stemming_strategy' => 'STEM_NONE', + 'dmsf_webdav' => '0', + 'dmsf_display_notified_recipients' => '0', + 'dmsf_global_title_format' => '', + 'dmsf_columns' => %w[title size modified version workflow author], + 'dmsf_webdav_ignore' => '^(\._|\.DS_Store$|Thumbs.db$)', + 'dmsf_webdav_disable_versioning' => '^\~\$|\.tmp$', + 'dmsf_keep_documents_locked' => '0', + 'dmsf_act_as_attachable' => '0', + 'dmsf_documents_email_from' => '', + 'dmsf_documents_email_reply_to' => '', + 'dmsf_documents_email_links_only' => '0', + 'dmsf_enable_cjk_ngrams' => '0', + 'dmsf_webdav_use_project_names' => '0', + 'dmsf_webdav_ignore_1b_file_for_authentication' => '1', + 'dmsf_projects_as_subfolders' => '0', + 'only_approval_zero_minor_version' => '0', + 'dmsf_max_notification_receivers_info' => 10, + 'office_bin' => 'libreoffice', + 'dmsf_global_menu_disabled' => '0', + 'dmsf_default_query' => '0', + 'empty_minor_version_by_default' => '0', + 'remove_original_documents_module' => '0', + 'dmsf_webdav_authentication' => 'Digest', + 'dmsf_really_delete_files' => '0' + } +end + +# Administration menu extension +Redmine::MenuManager.map :admin_menu do |menu| + menu.push :dmsf_approvalworkflows, :dmsf_workflows_path, + caption: :label_dmsf_workflow_plural, + icon: 'workflows', + html: { class: 'icon icon-workflows' }, + if: proc { |_| User.current.admin? } +end +# Project menu extension +Redmine::MenuManager.map :project_menu do |menu| + menu.push :dmsf, { controller: 'dmsf', action: 'show' }, + caption: :menu_dmsf, + before: :documents, + param: :id, + html: { class: 'icon icon-dmsf' } + # New menu extension + menu.push :dmsf_file, { controller: 'dmsf_upload', action: 'multi_upload' }, + caption: :label_dmsf_new_top_level_document, parent: :new_object + menu.push :dmsf_folder, { controller: 'dmsf', action: 'new' }, + caption: :label_dmsf_new_top_level_folder, + parent: :new_object +end +# Main menu extension +Redmine::MenuManager.map :top_menu do |menu| + menu.push :dmsf, { controller: 'dmsf', action: 'index' }, + caption: :menu_dmsf, + html: { class: 'icon-dmsf', category: :rest_extension_modules }, + if: proc { + User.current.allowed_to?(:view_dmsf_folders, nil, global: true) && + ActiveRecord::Base.connection.data_source_exists?('settings') && + !RedmineDmsf.dmsf_global_menu_disabled? + } +end + +Redmine::AccessControl.map do |map| + map.project_module :dmsf do |pmap| + pmap.permission :view_dmsf_file_revision_accesses, {}, read: true + pmap.permission :view_dmsf_file_revisions, {}, read: true + pmap.permission :view_dmsf_folders, { dmsf: %i[show index] }, read: true + pmap.permission :user_preferences, { dmsf_state: [:user_pref_save] }, require: :member + pmap.permission(:view_dmsf_files, + { + dmsf: %i[entries_operation entries_email download_email_entries add_email append_email + autocomplete_for_user], + dmsf_files: %i[show view thumbnail], + dmsf_workflows: [:log] + }, + read: true) + pmap.permission :email_documents, + { dmsf_public_urls: [:create] } + pmap.permission :folder_manipulation, + { + dmsf: %i[new create delete edit save edit_root save_root lock unlock notify_activate + notify_deactivate restore drop copymove], + dmsf_folder_permissions: %i[new append autocomplete_for_user], + dmsf_context_menus: [:dmsf] + } + pmap.permission :file_manipulation, + { + dmsf_files: %i[create_revision lock unlock delete_revision obsolete_revision notify_activate + notify_deactivate restore], + dmsf_upload: %i[upload_files upload commit_files commit delete_dmsf_attachment + delete_dmsf_link_attachment multi_upload], + dmsf_links: %i[new create destroy restore autocomplete_for_project autocomplete_for_folder], + dmsf_context_menus: [:dmsf] + } + pmap.permission :file_delete, + { + dmsf: %i[trash delete_entries empty_trash], + dmsf_files: [:delete], + dmsf_trash_context_menus: [:trash] + } + pmap.permission :force_file_unlock, {} + pmap.permission :file_approval, + { dmsf_workflows: %i[action new_action autocomplete_for_user start assign assignment] } + pmap.permission :manage_workflows, + { + dmsf_workflows: %i[index new create destroy show new_step add_step remove_step reorder_steps + update update_step delete_step edit] + } + pmap.permission :display_system_folders, {}, read: true + # Watchers + pmap.permission :view_dmsf_file_watchers, {}, read: true + pmap.permission :add_dmsf_file_watchers, { watchers: %i[new create append autocomplete_for_user] } + pmap.permission :delete_dmsf_file_watchers, { watchers: :destroy } + pmap.permission :view_dmsf_folder_watchers, {}, read: true + pmap.permission :add_dmsf_folder_watchers, { watchers: %i[new create append autocomplete_for_user] } + pmap.permission :delete_dmsf_folder_watchers, { watchers: :destroy } + pmap.permission :view_project_watchers, {}, read: true + pmap.permission :add_project_watchers, { watchers: %i[new create append autocomplete_for_user] } + pmap.permission :delete_project_watchers, { watchers: :destroy } + end +end + +# DMSF WebDAV digest token +Token.add_action :dmsf_webdav_digest, max_instances: 1, validity_time: nil + +Rails.application.configure do + # Rubyzip configuration + Zip.unicode_names = true + + # DMS custom fields + CustomFieldsHelper::CUSTOM_FIELDS_TABS << { name: 'DmsfFileRevisionCustomField', partial: 'custom_fields/index', + label: :dmsf } + + # Searchable modules + Redmine::Search.map do |search| + search.register :dmsf_files + search.register :dmsf_folders + end + + # Activities + Redmine::Activity.register :dmsf_file_revision_accesses, default: false + Redmine::Activity.register :dmsf_file_revisions +end diff --git a/jetbrains-variant-3.svg b/jetbrains-variant-3.svg new file mode 100644 index 00000000..52e7822a --- /dev/null +++ b/jetbrains-variant-3.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/dav4rack.rb b/lib/dav4rack.rb new file mode 100644 index 00000000..953a5b17 --- /dev/null +++ b/lib/dav4rack.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +require 'time' +require 'uri' +require 'nokogiri' +require 'ox' +require 'digest' +require 'rack' + +require "#{File.dirname(__FILE__)}/dav4rack/utils" +require "#{File.dirname(__FILE__)}/dav4rack/http_status" +require "#{File.dirname(__FILE__)}/dav4rack/resource" +require "#{File.dirname(__FILE__)}/dav4rack/handler" +require "#{File.dirname(__FILE__)}/dav4rack/controller" + +module Dav4rack + IS_18 = RUBY_VERSION[0, 3] == '1.8' +end diff --git a/lib/dav4rack/LICENSE b/lib/dav4rack/LICENSE new file mode 100644 index 00000000..a1a4441f --- /dev/null +++ b/lib/dav4rack/LICENSE @@ -0,0 +1,42 @@ +Current Dav4rack license: + +Copyright (c) 2010 Chris Roberts + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Original rack_dav source license: + +Copyright (c) 2009 Matthias Georgi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/dav4rack/README.md b/lib/dav4rack/README.md new file mode 100644 index 00000000..9f66258a --- /dev/null +++ b/lib/dav4rack/README.md @@ -0,0 +1,467 @@ +# Dav4rack - Web Authoring for Rack[![Build Status](https://travis-ci.org/planio-gmbh/dav4rack.svg?branch=master)](https://travis-ci.org/planio-gmbh/dav4rack) + +Dav4rack is a framework for providing WebDAV via Rack allowing content +authoring over HTTP. It is based off the [original RackDAV +framework](http://github.com/georgi/rack_dav) adding some useful new features: + +- Better resource support for building fully virtualized resource structures +- Generic locking as well as Resource level specific locking +- Interceptor middleware to provide virtual mapping to resources +- Mapped resource paths +- Authentication support +- Resource callbacks +- Remote file proxying (including sendfile support for remote files) +- Nokogiri based document parsing +- Ox based XML document building (for performance reasons) + +If you find issues, please create a new issue on github. If you have fixes, +please fork the repo and send me a pull request with your modifications. If you +are just here to use the library, enjoy! + + +## About this fork + +This is the [Planio](https://plan.io/redmine-hosting) fork of Dav4rack. The +master branch includes improvements and fixes done by @djgraham and +@tim-vandecasteele in their respective forks on Github. + +It also incorporates various fixes that were made as part of the redmine\_dmsf +plugin, as well as improvements done by ourselves during development of an +upcoming redmine document management plugin. + +Several core APIs were changed in the process so it will not be a straight +upgrade for applications that were developed with Dav4rack 0.3 (the last +released Gem version). + +## Install + +### Bundler + +To use this fork, include in your Gemfile: + + gem 'dav4rack', git: 'https://github.com/planio-gmbh/dav4rack.git', branch: 'master' + + +### Via RubyGems + + gem install dav4rack + +This will give you the last officially released version, which is *very* old. + + +## Documentation + +- [Dav4rack documentation](http://chrisroberts.github.com/dav4rack) + +## Quickstart + +If you just want to share a folder over WebDAV, you can just start a +simple server with: + + dav4rack + +This will start a Unicorn, Mongrel or WEBrick server on port 3000, which you +can connect to without authentication. Unicorn and Mongrel will be much more +responsive than WEBrick, so if you are having slowness issues, install one of +them and restart the dav4rack process. The simple file resource allows very +basic authentication which is used for an example. To enable it: + + dav4rack --username=user --password=pass + + +## Rack Handler + +Using Dav4rack within a rack application is pretty simple. A very slim +rackup script would look something like this: + + +```ruby + require 'rubygems' + require 'dav4rack' + + use Rack::CommonRails.logger + run Dav4rack::Handler.new(root: '/path/to/public/fileshare') +``` + +This will use the included FileResource and set the share path. However, +Dav4rack has some nifty little extras that can be enabled in the rackup script. +First, an example of how to use a custom resource: + +```ruby + run Dav4rack::Handler.new(resource_class: CustomResource, + custom: 'options', + passed: 'to resource') +``` + +Next, lets venture into mapping a path for our WebDAV access. In this example, +we will use default FileResource like in the first example, but instead of the +WebDAV content being available at the root directory, we will map it to a +specific directory: `/webdav/share/` + +```ruby + require 'rubygems' + require 'dav4rack' + + use Rack::CommonRails.logger + + app = Rack::Builder.new{ + map '/webdav/share/' do + run Dav4rack::Handler.new(root: '/path/to/public/fileshare') + end + }.to_app + run app +``` + +Aside from the `Builder#map` block, notice the new option passed to the Handler's +initialization, `:root_uri_path`. When Dav4rack receives a request, it will +automatically convert the request to the proper path and pass it to the +resource. + +Another tool available when building the rackup script is the Interceptor. The +Interceptor's job is to simply intercept WebDAV requests received up the path +hierarchy where no resources are currently mapped. For example, lets continue +with the last example but this time include the interceptor: + + +```ruby + require 'rubygems' + require 'dav4rack' + + use Rack::CommonRails.logger + app = Rack::Builder.new{ + map '/webdav/share/' do + run Dav4rack::Handler.new(root: '/path/to/public/fileshare') + end + map '/webdav/share2/' do + run Dav4rack::Handler.new(resource_class: CustomResource) + end + map '/' do + use Dav4rack::Interceptor, mappings: { + '/webdav/share/' => {resource_class: FileResource, custom: 'option'}, + '/webdav/share2/' => {resource_class: CustomResource} + } + use Rails::Rack::Static + run ActionController::Dispatcher.new + end + }.to_app + run app +``` + +In this example we have two WebDAV resources restricted by path. This means +those resources will handle requests to `/webdav/share/* and /webdav/share2/*` +but nothing above that. To allow webdav to respond, we provide the Interceptor. +The Interceptor does not provide any authentication support. It simply creates +a virtual file system view to the provided mapped paths. Once the actual +resources have been reached, authentication will be enforced based on the +requirements defined by the individual resource. Also note in the root map you +can see we are running a Rails application. This is how you can easily enable +Dav4rack with your Rails application. + + +## Custom Middleware + +This is an alternative way to integrate one or more webdav handlers into a +Rails app. It uses a custom middleware dispatching to any number of mounted +Dav4Rack handlers, handles OPTIONS requests outside the webdav namespaces for +interoperability with microsoft windows and lastly dispatches any remaining +requests to the main (Rails) application. + +```ruby + +class CustomMiddleware + + def initialize(app) + @rails_app = app + + @dav_app = Rack::Builder.new{ + map '/dav/' do + run Dav4rack::Handler.new(resource_class: CustomResource) + end + + map '/other/dav' do + run CustomDavHandler.new + end + }.to_app + end + + def call(env) + status, headers, body = @dav_app.call env + + # If the URL map generated by Rack::Builder did not find a matching path, + # it will return a 404 along with the X-Cascade header set to 'pass'. + if status == 404 and headers['X-Cascade'] == 'pass' + + # The MS web redirector webdav client likes to go up a level and try + # OPTIONS there. We catch that here and respond telling it that just + # plain HTTP is going on. + if 'OPTIONS'.casecmp(env['REQUEST_METHOD'].to_s) == 0 + [ '200', { 'Allow' => 'OPTIONS,HEAD,GET,PUT,POST,DELETE' }, [''] ] + else + # let Rails handle the request + @rails_app.call env + end + + else + [status, headers, body] + end + end + +end +``` + +You can add this middleware to your Rails app using + +```ruby +Rails.configuration.middleware.insert_before ActionDispatch::Cookies, CustomMiddleware +``` + +## Logging + +Dav4rack provides some simple logging in a Rails style format (simply for +consistency) so the output should look somewhat familiar. + + Dav4rack::Handler.new(resource_class: CustomResource, log_to: '/my/log/file') + +You can even specify the level of logging: + + Dav4rack::Handler.new(resource_class: CustomResource, log_to: ['/my/log/file', Rails.logger::DEBUG]) + +In order to use the Rails Rails.logger, just specify `log_to: Rails.logger`. + +## Custom Resources + +Creating your own resource is easy. Simply inherit the Dav4rack::Resource +class, and start redefining all the methods you want to customize. The +Dav4rack::Resource class only has implementations for methods that can be +provided extremely generically. This means that most things will require at +least some sort of implementation. However, because the Resource is defined so +generically, and the Controller simply passes the request on to the Resource, +it is easy to create fully virtualized resources. + +## Helpers + +There are some helpers worth mentioning that make things a little easier. + +First of all, take note that the `request` object will be an instance of `Dav4rack::Request`, which extends `Rack::Request` with some useful helpers. + +### Redirects and sending remote files + +If `request.client_allows_redirect?` is true, the currently connected client +will accept and properly use a 302 redirect for a GET request. Most clients do +not properly support this, which can be a real pain when working with +virtualized files that may be located some where else, like S3. To deal with +those clients that don't support redirects, a helper has been provided so +resources don't have to deal with proxying themselves. The Dav4rack::RemoteFile +is a modified Rack::File that can do some interesting things. First, lets look +at its most basic use: + + class MyResource < Dav4rack::Resource + def setup + @item = method_to_fill_this_properly + end + + def get + if(request.client_allows_redirect?) + response.redirect item[:url] + else + response.body = Dav4rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type) + OK + end + end + end + +This is a simple proxy. When Rack receives the RemoteFile, it will pull a chunk of data from object, which in turn pulls it from the socket, and +sends it to the user over and over again until the EOF is reached. This much the same method that Rack::File uses but instead we are pulling +from a socket rather than an actual file. Now, instead of proxying these files from a remote server every time, lets cache them: + + response.body = Dav4rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :cache_directory => '/tmp') + +Providing the `:cache_directory` will let RemoteFile cache the items locally, +and then search for them on subsequent requests before heading out to the +network. The cached file name is based off the SHA1 hash of the file path, size +and last modified time. It is important to note that for services like S3, the +path will often change, making this cache pretty worthless. To combat this, we +can provide a reference to use instead: + + response.body = Dav4rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :cache_directory => '/tmp', :cache_ref => item[:static_url]) + +These methods will work just fine, but it would be really nice to just let +someone else deal with the proxying and let the process get back to dealing +with actual requests. RemoteFile will happily do that as long as the frontend +server is setup correctly. Using the sendfile approach will tell the RemoteFile +to simply pass the headers on and let the server deal with doing the actual +proxying. First, lets look at an implementation using all the features, and +then degrade that down to the bare minimum. These examples are NGINX specific, +but are based off the Rack::Sendfile implementation and as such should be +applicable to other servers. First, a simplified NGINX server block: + + server { + listen 80; + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-Sendfile-Type X-Accel-Redirect; + proxy_set_header X-Accel-Remote-Mapping webdav_redirect + proxy_pass http://my_app_server; + } + + location ~* /webdav_redirect { + internal; + resolver 127.0.0.1; + set $r_host $upstream_http_redirect_host; + set $r_url $upstream_http_redirect_url; + proxy_set_header Authorization ''; + proxy_set_header Host $r_host; + proxy_max_temp_file_size 0; + proxy_pass $r_url; + } + } + +With this in place, the parameters for the RemoteFile change slightly: + + response.body = Dav4rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => true) + +The RemoteFile will automatically take care of building out the correct path and sending the proper headers. If the X-Accel-Remote-Mapping header +is not available, you can simply pass the value: + + response.body = Dav4rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => true, :sendfile_prefix => 'webdav_redirect') + +And if you don't have the X-Sendfile-Type header set, you can fix that by changing the value of :sendfile: + + response.body = Dav4rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => 'X-Accel-Redirect', :sendfile_prefix => 'webdav_redirect') + +And if you have none of the above because your server hasn't been configured for sendfile support, you're out of luck until it's configured. + +## Authentication + +Authentication is performed on a per Resource basis. The Controller object will +call `#authenticate` on any Resources it handles requests for. Basic +Authentication information from the request will be passed to the method. +Depending on the result, the Controller will either continue on with the +request, or send a 401 Unauthorized response. + +Override `Resource#authentication_realm` and `Resource#authentication_error_msg` to customize the realm name and response content for authentication failures. + +Authentication can also be implemented using callbacks, as discussed below. + +## Callbacks + +*Deprecated*. This feature will most probably be removed in the future. + +If you want to implement general before/after logic for every request, use a +custom controller class and override `#process`. + + +Resources can make use of callbacks to easily apply permissions, authentication or any other action that needs to be performed before or after any or all +actions. Callbacks are applied to all publicly available methods. This is important for methods used internally within the resource. Methods not meant +to be called by the Controller, or anyone else, should be scoped protected or private to reduce the interaction with callbacks. + +Callbacks can be called before or after a method call. For example: + + class MyResource < Dav4rack::Resource + before do |resource, method_name| + resource.send(:my_authentication_method) + end + + after do |resource, method_name| + puts "#{Time.now} -> Completed: #{resource}##{method_name}" + end + + private + + def my_authentication_method + true + end + end + +In this example MyResource#my_authentication_method will be called before any public method is called. After any method has been called a status +line will be printed to STDOUT. Running callbacks before/after every method call is a bit much in most cases, so callbacks can be applied to specific +methods: + + class MyResource < Dav4rack::Resource + before_get do |resource| + puts "#{Time.now} -> Received GET request from resource: #{resource}" + end + end + +In this example, a simple status line will be printed to STDOUT before the MyResource#get method is called. The current resource object is always +provided to callbacks. The method name is only provided to the generic before/after callbacks. + +Something very handy for dealing with the mess of files OS X leaves on the system: + + class MyResource < Dav4rack::Resource + after_unlock do |resource| + resource.delete if resource.name[0,1] == '.' + end + end + +Because OS X implements locking correctly, we can wait until it releases the lock on the file, and remove it if it's a hidden file. + +Callbacks are called in the order they are defined, so you can easily build callbacks off each other. Like this example: + + class MyResource < Dav4rack::Resource + before do |resource, method_name| + resource.DAV_authenticate unless resource.user.is_a?(User) + raise Unauthorized unless resource.user.is_a?(User) + end + before do |resource, method_name| + resource.user.allowed?(method_name) + end + end + +In this example, the second block checking User#allowed? can count on Resource#user being defined because the blocks are called in +order, and if the Resource#user is not a User type, an exception is raised. + +### Avoiding callbacks + +Something special to notice in the last example is the DAV_ prefix on authenticate. Providing the DAV_ prefix will prevent +any callbacks being applied to the given method. This allows us to provide a public method that the callback can access on the resource +without getting stuck in a loop. + +## Software using Dav4rack! + +* {meishi}[https://github.com/inferiorhumanorgans/meishi] - Lightweight CardDAV implementation in Rails +* {dav4rack_ext}[https://github.com/schmurfy/dav4rack_ext] - CardDAV extension. (CalDAV planned) + +## Issues/Bugs/Questions + +### Known Issues + +- OS X Finder PUT fails when using NGINX (this is due to NGINX's lack of + chunked transfer encoding in earlier versions). Use a recent version of + NGINX. +- Windows WebDAV mini-redirector - this client is very broken. Windows from + version 7 onwards however should work fine with the `OPTIONS` handling + addition demonstrated above. +- Lots of unimplemented parts of the webdav spec (patches always welcome). Run + `test/litmus_all.sh` to see what works and what doesnt. + + +### Unknown Issues + +Please report issues at github: http://github.com/planio-gmbh/dav4rack/issues +Include as much information about the environment as possible (especially client OS / software). + +## Contributors + +A big thanks to everyone contributing to help make this project better. + +* [clyfe](http://github.com/clyfe) +* [antiloopgmbh](http://github.com/antiloopgmbh) +* [krug](http://github.com/krug) +* [teefax](http://github.com/teefax) +* [buffym](https://github.com/buffym) +* [jbangert](https://github.com/jbangert) +* [doxavore](https://github.com/doxavore) +* [spicyj](https://github.com/spicyj) +* [TurchenkoAlex](https://github.com/TurchenkoAlex) +* [exabugs](https://github.com/exabugs) +* [inferiorhumanorgans](https://github.com/inferiorhumanorgans) +* [schmurfy](https://github.com/schmurfy) +* [pifleo](https://github.com/pifleo) +* [mlmorg](https://github.com/mlmorg) + +## License + +Just like RackDAV before it, this software is distributed under the MIT license. diff --git a/lib/dav4rack/controller.rb b/lib/dav4rack/controller.rb new file mode 100644 index 00000000..d85ab441 --- /dev/null +++ b/lib/dav4rack/controller.rb @@ -0,0 +1,382 @@ +# frozen_string_literal: true + +require "#{File.dirname(__FILE__)}/uri" +require "#{File.dirname(__FILE__)}/destination_header" +require "#{File.dirname(__FILE__)}/request" +require "#{File.dirname(__FILE__)}/xml_elements" +require "#{File.dirname(__FILE__)}/xml_response" + +module Dav4rack + # Controller + class Controller + include Dav4rack::HttpStatus + include Dav4rack::Utils + + attr_reader :request, :response, :resource + + # request:: Dav4rack::Request + # response:: Rack::Response + # options:: Options hash + # Create a new Controller. + # + # options will be passed on to the resource + def initialize(request, response, options = {}) + @options = options + @request = request + @response = response + @dav_extensions = options[:dav_extensions] + @always_include_dav_header = !options[:always_include_dav_header].nil? + @allow_unauthenticated_options_on_root = !options[:allow_unauthenticated_options_on_root].nil? + setup_resource + add_dav_header if @always_include_dav_header + end + + # main entry point, called by the Handler + def process + status = skip_authorization? || authenticate? ? process_action || OK : HttpStatus::Unauthorized + rescue HttpStatus::Status => e + status = e + ensure + if status + response.status = status.code + if status.code == 401 + response.body = authentication_error_message + response['WWW-Authenticate'] = "Basic realm=\"#{authentication_realm}\"" + end + end + end + + private + + # delegates to the handler method matching this requests http method. + # must return an HttpStatus. If nil / false, the resulting status will be + # 200/OK + def process_action + send request.request_method.downcase + end + + # if true, resource#authenticate will never be called + def skip_authorization? + # Microsoft Web Client workaround (from RedmineDmsf plugin): + # + # MS web client will not attempt any authentication (it'd seem) + # until it's acknowledged a completed OPTIONS request. + # + # If the request method is OPTIONS return true, controller will simply + # call the options method within, which accesses nothing, just returns + # headers about the dav env. + @allow_unauthenticated_options_on_root && request.request_method.casecmp('options').zero? && + (request.path_info == '/' || request.path_info.empty?) + end + + # Return response to OPTIONS + def options + status = resource.options request, response + if status == OK + add_dav_header + response['Allow'] ||= 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK' + response['Ms-Author-Via'] ||= 'DAV' + end + status + end + + # Return response to HEAD + def head + return NotFound unless resource.exist? + + res = resource.head(request, response) + if res == OK + response['Etag'] ||= resource.etag + response['Content-Type'] ||= resource.content_type + response['Content-Length'] ||= resource.content_length.to_s + response['Last-Modified'] ||= resource.last_modified.httpdate + end + res + end + + # Return response to GET + def get + return NotFound unless resource.exist? + + res = resource.get(request, response) + if res == OK && !resource.collection? + response['Etag'] ||= resource.etag + response['Content-Type'] ||= resource.content_type + response['Content-Length'] ||= resource.content_length.to_s + response['Last-Modified'] ||= resource.last_modified.httpdate + end + res + end + + # Return response to PUT + def put + if request.get_header('HTTP_CONTENT_RANGE') + # An origin server that allows PUT on a given target resource MUST send + # a 400 (Bad Request) response to a PUT request that contains a + # Content-Range header field. + # Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4 + Rails.logger.error 'Content-Range on PUT requests is forbidden.' + BadRequest + elsif resource.collection? + Forbidden + elsif !resource.parent.exist? || !resource.parent.collection? + Conflict + else + resource.lock_check if resource.supports_locking? + status = resource.put(request) + response['Location'] ||= request.url_for resource.path if status == Created + response.body = response['Location'] || '' + status + end + end + + # Return response to POST + def post + resource.post request, response + end + + # Return response to DELETE + def delete + return NotFound unless resource.exist? + + resource.lock_check if resource.supports_locking? + resource.delete + end + + # Return response to MKCOL + def mkcol + return UnsupportedMediaType if request.content_length.to_i.positive? + return MethodNotAllowed if resource.exist? + + resource.lock_check if resource.supports_locking? + status = resource.make_collection + + return status unless resource.use_compat_mkcol_response? + + url = request.url_for resource.path, collection: true + r = XmlResponse.new(response, resource.namespaces) + r.multistatus do |xml| + xml << r.response(url, status) + end + MultiStatus + end + + # Move Resource to new location. + def move + return NotFound unless resource.exist? + return BadRequest unless request.depth == 'infinity' + + dest = request.destination + return BadRequest unless dest + + status = dest.validate(host: request.host, resource_path: resource.path) + return status if status + + resource.lock_check if resource.supports_locking? + resource.move dest.path_info + end + + # Return response to COPY + def copy + return NotFound unless resource.exist? + return BadRequest unless request.depth == 'infinity' || request.depth.zero? + + dest = request.destination + return BadRequest unless dest + + status = dest.validate(host: request.host, resource_path: resource.path) + return status if status + + resource.copy dest.path_info + end + + # Return response to PROPFIND + def propfind + return NotFound unless resource.exist? + + ns = request.ns + document = request.document if request.content_length.to_i.positive? + propfind = document.xpath("//#{ns}propfind") if document + + # propname request + if propfind&.xpath("//#{ns}propname")&.first + r = XmlResponse.new(response, resource.namespaces) + r.multistatus do |xml| + xml << Ox::Raw.new(resource.propnames_xml_with_depth) + end + return MultiStatus + end + + properties = if propfind.blank? || propfind.xpath("//#{ns}allprop").first + resource.properties + elsif (prop = propfind.xpath("//#{ns}prop").first) + prop.children + .find_all(&:element?) + .filter_map do |item| + # We should do this, but Nokogiri transforms prefix w/ null href + # into something valid. Oops. + # TODO: Hacky grep fix that's horrible + hsh = to_element_hash(item) + if hsh.namespace.nil? && !ns.empty? && prop.to_s.scan(/<#{item.name}[^>]+xmlns=""/).empty? + return BadRequest + end + + hsh + end + else + Rails.logger.error 'No properties found' + raise BadRequest + end + + properties = properties.map { |property| { element: property } } + properties = resource.propfind_add_additional_properties(properties) + prop_xml = resource.properties_xml_with_depth(get: properties) + r = XmlResponse.new(response, resource.namespaces) + r.multistatus do |xml| + xml << r.raw(prop_xml) + end + MultiStatus + end + + # Return response to PROPPATCH + def proppatch + return NotFound unless resource.exist? + return BadRequest unless (doc = request.document) + + resource.lock_check if resource.supports_locking? + properties = {} + actions = %w[set remove] + doc.xpath("/#{request.ns}propertyupdate").children.each do |element| + action = element.name + next unless actions.include?(action) + + properties[action] ||= [] + next unless (prp = element.children.detect { |e| e.name == 'prop' }) + + prp.children.each do |elm| + next if elm.name == 'text' + + properties[action] << { element: to_element_hash(elm), value: elm.text } + end + end + + r = XmlResponse.new(response, resource.namespaces) + r.multistatus do |xml| + xml << Ox::Raw.new(resource.properties_xml_with_depth(properties)) + end + MultiStatus + end + + # Lock current resource + # NOTE: This will pass an argument hash to Resource#lock and + # wait for a success/failure response. + def lock + depth = request.depth + return BadRequest unless request.depth == 'infinity' || request.depth.zero? + + asked = { depth: depth } + timeout = request.env['Timeout'] + asked[:timeout] = timeout.split(',').map(&:strip) if timeout + ns = request.ns + doc = request.document + lockinfo = doc&.xpath("//#{ns}lockinfo") + if lockinfo + # + # + # + # + # + # + # + # + # uraab + # + # + asked[:scope] = lockinfo.xpath("//#{ns}lockscope").children.find_all(&:element?).map(&:name).first + asked[:type] = lockinfo.xpath("#{ns}locktype").children.find_all(&:element?).map(&:name).first + asked[:owner] = lockinfo.xpath("//#{ns}owner").children.map(&:text).first + end + + r = XmlResponse.new(response, resource.namespaces) + begin + lock_time, locktoken = resource.lock(asked) + + r.render_lockdiscovery( + time: lock_time, + token: locktoken, + depth: asked[:depth].to_s, + scope: asked[:scope], + type: asked[:type], + owner: asked[:owner] + ) + + response.headers['Lock-Token'] = "<#{locktoken}>" + resource.exist? ? OK : Created + rescue LockFailure => e + begin + resource.lock_check + rescue StandardError => _ex + return Locked + end + r.multistatus do |xml| + e.path_status.each_pair do |path, status| + xml << r.response(path, status) + end + end + MultiStatus + end + end + + # Unlock current resource + def unlock + resource.unlock request.lock_token + end + + # Perform authentication + # + # implement your authentication by overriding Resource#authenticate + def authenticate? + uname = nil + password = nil + if request.authorization? + auth = Rack::Auth::Basic::Request.new(request.env) + if auth.basic? && auth.credentials + uname = auth.credentials[0] + password = auth.credentials[1] + end + end + resource.authenticate? uname, password + end + + def authentication_error_message + resource.authentication_error_message + end + + def authentication_realm + resource.authentication_realm + end + + # override this for custom resource initialization + def setup_resource + @resource = resource_class.new(request.unescaped_path_info, request, response, @options) + end + + # Class of the resource in use + def resource_class + @options[:resource_class] + end + + def add_dav_header + return if response['Dav'] + + dav_support = ['1'] + if resource.supports_locking? + # compliance is resource specific, only advertise 2 (locking) if + # supported on the resource. + dav_support << '2' + end + dav_support += @dav_extensions if @dav_extensions + response['Dav'] = dav_support * ', ' + end + end +end diff --git a/lib/dav4rack/destination_header.rb b/lib/dav4rack/destination_header.rb new file mode 100644 index 00000000..aa17937f --- /dev/null +++ b/lib/dav4rack/destination_header.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'addressable/uri' + +module Dav4rack + # Destination header + class DestinationHeader + attr_reader :host, :path, :path_info + + # uri is expected to be a Dav4rack::Uri instance + def initialize(uri) + @host = uri.host + @path = uri.path + @path_info = uri.path_info + # nil path info means path is outside the realm of script_name + raise ArgumentError, "invalid destination header value: #{uri}" unless @path_info + end + + # host must be the same, but path must differ + def validate(host: nil, resource_path: nil) + if host && self.host && self.host != host + Dav4rack::HttpStatus::BadGateway + elsif resource_path && path_info == resource_path + Dav4rack::HttpStatus::Forbidden + end + end + end +end diff --git a/lib/dav4rack/file_resource_lock.rb b/lib/dav4rack/file_resource_lock.rb new file mode 100644 index 00000000..d30487ce --- /dev/null +++ b/lib/dav4rack/file_resource_lock.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require 'pstore' + +module Dav4rack + # File resource lock + class FileResourceLock + attr_accessor :path, :token, :timeout, :depth, :user, :scope, :kind, :owner + attr_reader :created_at + attr_writer :root + + class << self + def explicitly_locked?(path, croot = nil) + store = init_pstore(croot) + !store.transaction(true) { store[:paths][path] }.nil? + end + + def implicitly_locked?(path, croot = nil) + store = init_pstore(croot) + locked = store.transaction(true) do + store[:paths].keys.detect do |check| + check.start_with? path + end + end + !locked.nil? + end + + def explicit_locks(_path, _croot, _args = {}) + # Nothing to do + end + + def implicit_locks(_path) + # Nothing to do + end + + def find_by_path(path, croot = nil) + lock = self.class.new(path: path, root: croot) + lock.token.nil? ? nil : lock + end + + def find_by_token(token, croot = nil) + store = init_pstore(croot) + struct = store.transaction(true) { store[:tokens][token] } + struct ? new(path: struct[:path], root: croot) : nil + end + + def generate(path, user, token, croot) + lock = new(root: croot) + lock.user = user + lock.path = path + lock.token = token + lock.save + lock + end + + def root + @root || '/tmp/dav4rack' + end + + def init_pstore(croot) + path = File.join(croot, '.attribs', 'locks.pstore') + FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path)) + store = IS_18 ? PStore.new(path) : PStore.new(path, true) + store.transaction do + unless store[:paths] + store[:paths] = {} + store[:tokens] = {} + store.commit + end + end + store + end + end + + def initialize(args = {}) + @path = args[:path] + @root = args[:root] + @owner = args[:owner] + @store = init_pstore(@root) + @max_timeout = args[:max_timeout] || 86_400 + @default_timeout = args[:max_timeout] || 60 + load_if_exists! + @new_record = true if token.nil? + end + + def owner?(user) + user == owner + end + + def reload + load_if_exists + self + end + + def remaining_timeout + t = timeout.to_i - (Time.created.to_i - created_at.to_i) + t.negative? ? 0 : t + end + + def save + struct = { + path: path, + token: token, + timeout: timeout, + depth: depth, + created_at: Time.current, + owner: owner + } + @store.transaction do + @store[:paths][path] = struct + @store[:tokens][token] = struct + @store.commit + end + @new_record = false + self + end + + def destroy + @store.transaction do + @store[:paths].delete path + @store[:tokens].delete token + @store.commit + end + nil + end + + private + + def load_if_exists! + struct = @store.transaction do + @store[:paths][path] + end + if struct + @path = struct[:path] + @token = struct[:token] + @timeout = struct[:timeout] + @depth = struct[:depth] + @created_at = struct[:created_at] + @owner = struct[:owner] + end + self + end + + def init_pstore(croot = nil) + self.class.init_pstore croot || @root + end + end +end diff --git a/lib/dav4rack/handler.rb b/lib/dav4rack/handler.rb new file mode 100644 index 00000000..d2c97579 --- /dev/null +++ b/lib/dav4rack/handler.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Dav4rack + # Handler + class Handler + include Dav4rack::HttpStatus + + # Options: + # + # - resource_class: your Resource implementation + # - controller_class: custom Controller implementation (optional). + # - root_uri_path: Path the handler is mapped to. Any resources + # instantiated will only see the part of the path below this root. + # - recursive_propfind_allowed (true) : set to false to disable + # potentially expensive Depth: Infinity propfinds + # - all options are passed on to your resource implementation and are + # accessible there as @options. + # + def initialize(options = {}) + @options = options.dup + end + + def call(env) + start = Time.current + r = setup_request env + response = Rack::Response.new + + Rails.logger.info do + "Processing WebDAV request: #{r.path} (for #{r.ip} at #{Time.current}) [#{r.request_method}]" + end + + controller = setup_controller(r, response) + controller.process + postprocess_response response + + # Apache wants the body dealt with, so just read it and junk it + buf = r.body + buf = r.body.read(8_192) while buf + + Rails.logger.debug { "Response String:\n#{response.body}" } if response.body.is_a?(String) + Rails.logger.info do + duration = ((Time.current.to_f - start.to_f) * 1_000).to_i + "Completed in: #{duration} ms | #{response.status} [#{r.url}]" + end + + response.finish + rescue StandardError => e + Rails.logger.error "WebDAV Error: #{e.message}" + raise e + end + + private + + def postprocess_response(response) + response['Content-Length'] ||= response.body.length.to_s if response.body.is_a?(String) + response.body = [response.body] unless response.body.respond_to?(:each) + end + + def setup_request(env) + ::Dav4rack::Request.new env, @options + end + + def setup_controller(request, response) + controller_class.new request, response, @options + end + + def controller_class + @options[:controller_class] || ::Dav4rack::Controller + end + end +end diff --git a/lib/dav4rack/http_status.rb b/lib/dav4rack/http_status.rb new file mode 100644 index 00000000..1572fe08 --- /dev/null +++ b/lib/dav4rack/http_status.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Dav4rack + # HTTP status + module HttpStatus + # Status + class Status < StandardError + delegate :code, to: :class + delegate :reason_phrase, to: :class + delegate :status_line, to: :class + delegate :to_i, to: :class + + class << self + attr_accessor :code, :reason_phrase + alias to_i code + + def status_line + "#{code} #{reason_phrase}" + end + end + end + + STATUS_MESSAGES = { + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Request Range Not Satisfiable', + 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 507 => 'Insufficient Storage' + }.freeze + + STATUS_CLASSES = {}.tap do |hsh| + STATUS_MESSAGES.each do |code, reason_phrase| + klass = Class.new(Status) + klass.code = code + klass.reason_phrase = reason_phrase + klass_name = reason_phrase.gsub(/[ \-]/, '') + const_set klass_name, klass + hsh[code] = klass + end + end.freeze + end +end diff --git a/lib/dav4rack/interceptor.rb b/lib/dav4rack/interceptor.rb new file mode 100644 index 00000000..a9c66e97 --- /dev/null +++ b/lib/dav4rack/interceptor.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "#{File.dirname(__FILE__)}/interceptor_resource" + +module Dav4rack + # Interceptor + class Interceptor + def initialize(app, args = {}) + @roots = args[:mappings].keys + @args = args + @app = app + @intercept_methods = %w[OPTIONS PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK] + @intercept_methods -= args[:ignore_methods] if args[:ignore_methods] + end + + def call(env) + path = env['PATH_INFO'].downcase + method = env['REQUEST_METHOD'].upcase + app = nil + if @roots.detect { |x| path =~ %r{^#{Regexp.escape(x.downcase)}/?} }.nil? && @intercept_methods.include?(method) + app = Dav4rack::Handler.new( + resource_class: InterceptorResource, + mappings: @args[:mappings], + log_to: @args[:log_to] + ) + end + app ? app.call(env) : @app.call(env) + end + end +end diff --git a/lib/dav4rack/interceptor_resource.rb b/lib/dav4rack/interceptor_resource.rb new file mode 100644 index 00000000..8e5af28b --- /dev/null +++ b/lib/dav4rack/interceptor_resource.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +module Dav4rack + # Interceptor resource + class InterceptorResource < Resource + attr_reader :path, :options + + def initialize(*args) + super + @root_paths = @options[:mappings].keys + @mappings = @options[:mappings] + end + + def children + childs = @root_paths.grep(/^#{Regexp.escape(@path)}/) + childs.map { |a| child a.gsub(/^#{Regexp.escape(@path)}/, '').split('/').delete_if(&:empty?).first }.flatten + end + + def collection? + true if exist? + end + + def exist? + !@root_paths.grep(/^#{Regexp.escape(@path)}/).empty? + end + + def creation_date + Time.current + end + + def last_modified + Time.current + end + + def last_modified=(time) + # Nothing to do + end + + def etag + Digest::SHA1.hexdigest @path + end + + def content_type + 'text/html' + end + + def content_length + 0 + end + + def get(_request, _response) + raise Forbidden + end + + def put(_request) + raise Forbidden + end + + def post(_request, _response) + raise Forbidden + end + + def delete + raise Forbidden + end + + def copy(_dest) + raise Forbidden + end + + def move(_dest) + raise Forbidden + end + + def make_collection + raise Forbidden + end + + def ==(other) + path == other.path + end + + def name + ::File.basename path + end + + def display_name + ::File.basename path.to_s + end + + def child(name, options = {}) + new_path = path.dup + new_path = "/#{new_path}" unless new_path[0, 1] == '/' + new_path.slice!(-1) if new_path[-1, 1] == '/' + name = "/#{name}" unless name[-1, 1] == '/' + new_path = "#{new_path}#{name}" + new_public = public_path.dup + new_public = "/#{new_public}" unless new_public[0, 1] == '/' + new_public.slice!(-1) if new_public[-1, 1] == '/' + new_public = "#{new_public}#{name}" + if (key = @root_paths.find { |x| new_path =~ %r{^#{Regexp.escape(x.downcase)}/?} }) + @mappings[key][:resource_class].new(new_public, new_path.gsub(key, ''), request, response, + { root_uri_path: key, user: @user }.merge(options).merge(@mappings[key])) + else + self.class.new new_public, new_path, request, response, { user: @user }.merge(options) + end + end + + def descendants + list = [] + children.each do |child| + list << child + list.concat(child.descendants) + end + list + end + end +end diff --git a/lib/dav4rack/lock.rb b/lib/dav4rack/lock.rb new file mode 100644 index 00000000..d3371df2 --- /dev/null +++ b/lib/dav4rack/lock.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Dav4rack + # Lock + class Lock + attr_reader :store + + def initialize(args = {}) + @args = args + @store = nil + @args[:created_at] = Time.current + @args[:updated_at] = Time.current + end + + def store=(store) + raise TypeError, 'Expecting LockStore' unless store.respond_to? :remove + + @store = store + end + + def destroy + @store&.remove self + end + + def remaining_timeout + @args[:timeout].to_i - (Time.now.to_i - @args[:created_at].to_i) + end + + def respond_to_missing?(_name, _include_private) + true + end + + def method_missing(*args) + if @args.key?(args.first.to_sym) + @args[args.first.to_sym] + elsif args.first.to_s[-1, 1] == '=' + @args[args.first.to_s[0, args.first.to_s.length - 1].to_sym] = args[1] + else + raise NoMethodError, "Undefined method #{args.first} for #{self}" + end + end + end +end diff --git a/lib/dav4rack/lock_store.rb b/lib/dav4rack/lock_store.rb new file mode 100644 index 00000000..576bf068 --- /dev/null +++ b/lib/dav4rack/lock_store.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "#{File.dirname(__FILE__)}/lock" + +module Dav4rack + # Lock store + class LockStore + class << self + def create + @locks_by_path = {} + @locks_by_token = {} + end + + def add(lock) + @locks_by_path[lock.path] = lock + @locks_by_token[lock.token] = lock + end + + def remove(lock) + @locks_by_path.delete lock.path + @locks_by_token.delete lock.token + end + + def find_by_path(path) + @locks_by_path.filter_map { |lpath, lock| lpath == path && lock.remaining_timeout.positive? ? lock : nil } + .first + end + + def find_by_token(token) + @locks_by_token.filter_map do |ltoken, lock| + ltoken == token && lock.remaining_timeout.positive? ? lock : nil + end.first + end + + def explicit_locks(path) + @locks_by_path.filter_map { |lpath, lock| lpath == path && lock.remaining_timeout.positive? ? lock : nil } + end + + def implicit_locks(path) + @locks_by_path.filter_map do |lpath, lock| + lpath =~ /^#{Regexp.escape(path)}/ && lock.remaining_timeout.positive? && lock.depth.positive? ? lock : nil + end + end + + def explicitly_locked?(path) + !explicit_locks(path).empty? + end + + def implicitly_locked?(path) + !implicit_locks(path).empty? + end + + def generate(path, user, token) + l = Lock.new(path: path, user: user, token: token) + l.store = self + add l + l + end + end + end +end + +Dav4rack::LockStore.create diff --git a/lib/dav4rack/request.rb b/lib/dav4rack/request.rb new file mode 100644 index 00000000..158374f7 --- /dev/null +++ b/lib/dav4rack/request.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +require "#{File.dirname(__FILE__)}/uri" +require 'addressable/uri' + +module Dav4rack + # Request + class Request < Rack::Request + # Root URI path for the resource + attr_reader :root_uri_path + + # options: + # + # recursive_propfind_allowed (true) : set to false to disable + # potentially expensive recursive propfinds + # + def initialize(env, options = {}) + super(env) + @options = { recursive_propfind_allowed: true }.merge options + self.path_info = expand_path path_info + end + + def authorization? + !!authorization + end + + def authorization + get_header('HTTP_AUTHORIZATION') || + get_header('X-HTTP_AUTHORIZATION') || + get_header('X_HTTP_AUTHORIZATION') || + get_header('REDIRECT_X_HTTP_AUTHORIZATION') + end + + # path relative to root uri + def unescaped_path_info + @unescaped_path_info ||= self.class.unescape_path path_info + end + + # the full path (script_name aka rack mount point + path_info) + def unescaped_path + @unescaped_path ||= self.class.unescape_path path + end + + def self.unescape_path(path) + p = path.dup + p.force_encoding 'UTF-8' + Addressable::URI.unencode p + end + + # Namespace being used within XML document + def ns(wanted_uri = XmlElements::DAV_NAMESPACE) + if document && (root = document.root) && (ns_defs = root.namespace_definitions) && !ns_defs.empty? + result = ns_defs.detect { |nd| nd.href == wanted_uri } || ns_defs.first + result = result.prefix.nil? ? 'xmlns' : result.prefix.to_s + result += ':' unless result.empty? + result + else + '' + end + end + + # Lock token if provided by client + def lock_token + get_header 'HTTP_LOCK_TOKEN' + end + + # Requested depth + def depth + @depth ||= if (d = get_header('HTTP_DEPTH')) && %w[0 1].include?(d) + d.to_i + elsif infinity_depth_allowed? + 'infinity' + else + 1 + end + end + + # Destination header + def destination + unless @destination + h = get_header('HTTP_DESTINATION') + @destination = DestinationHeader.new Dav4rack::Uri.new(h, script_name: script_name) if h + end + @destination + end + + # Overwrite is allowed + def overwrite? + get_header('HTTP_OVERWRITE').to_s.casecmp('F').zero? + end + + # content_length as a Fixnum (nil if the header is unset / empty) + def content_length + length = super || get_header('HTTP_X_EXPECTED_ENTITY_LENGTH') + length&.to_i + end + + # parsed XML request body if any (Nokogiri XML doc) + def document + @request_document ||= parse_request_body if content_length&.positive? + end + + # builds a URL for path using this requests scheme, host, port and + # script_name + # path must be valid UTF-8 and will be url encoded by this method + def url_for(rel_path, collection: false) + path = path_for rel_path, collection: collection + # do not include the port when it's the standard http(s) port + skip_port = (port == 80 && scheme == 'http') || (port == 443 && scheme == 'https') + "#{scheme}://#{host}#{":#{port}" unless skip_port}#{path}" + end + + # returns an url encoded, absolute path for the given relative path + def path_for(rel_path, collection: false) + path = Addressable::URI.encode_component rel_path, Addressable::URI::CharacterClasses::PATH + path << '/' if collection && path[-1] != '/' + "#{script_name}#{expand_path path}" + end + + # returns the given path, but with the leading script_name removed. Will + # return nil if the path does not begin with the script_name + def path_info_for(full_path, script_name: self.script_name) + uri = Dav4rack::Uri.new full_path, script_name: script_name + uri.path_info + end + + # expands '/foo/../bar' to '/bar', peserving trailing slash and normalizing + # consecutive slashes. adds a leading slash if missing + def expand_path(path) + path = path.squeeze '/' + path.prepend '/' unless path[0] == '/' + collection = path.end_with?('/') + path = ::File.expand_path path + path << '/' if collection && !path.end_with?('/') + # remove a drive letter in Windows + path.gsub(%r{^([^/]*)/}, '/') + end + + REDIRECTABLE_CLIENTS = [ + /cyberduck/i, + /konqueror/i + ].freeze + + # Does client allow GET redirection + # TODO: Get a comprehensive list in here. + # TODO: Allow this to be dynamic so users can add regexes to match if they know of a client + # that can be supported that is not listed. + def client_allows_redirect? + ua = user_agent + REDIRECTABLE_CLIENTS.any? { |re| ua =~ re } + end + + def get_header(name) + @env[name] + end + + private + + # true if Depth: Infinity is allowed for this request. + # + # http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND + # Servers ... should support "infinity" requests. In practice, + # support for infinite-depth requests may be disabled, due to the + # performance and security concerns associated with this behavior + def infinity_depth_allowed? + request_method != 'PROPFIND' or @options[:recursive_propfind_allowed] + end + + def parse_request_body + Nokogiri.XML(body.read, &:strict) if body + rescue StandardError => e + Rails.logger.error e.message + raise Dav4rack::HttpStatus::BadRequest + end + end +end diff --git a/lib/dav4rack/resource.rb b/lib/dav4rack/resource.rb new file mode 100644 index 00000000..eca3a2e1 --- /dev/null +++ b/lib/dav4rack/resource.rb @@ -0,0 +1,647 @@ +# frozen_string_literal: true + +require 'uuidtools' +require "#{File.dirname(__FILE__)}/lock_store" +require "#{File.dirname(__FILE__)}/xml_elements" + +module Dav4rack + # Lock failure + class LockFailure < RuntimeError + attr_reader :path_status + + def initialize(*args) + super + @path_status = {} + end + + def add_failure(path, status) + @path_status[path] = status + end + end + + # Resource + class Resource + include Dav4rack::Utils + include Dav4rack::XmlElements + + attr_reader :path, :request, :response, :propstat_relative_path, :root_xml_attributes, :namespaces + + attr_accessor :user + + @blocks = {} + + class << self + # This lets us define a bunch of before and after blocks that are + # either called before all methods on the resource, or only specific + # methods on the resource + def respond_to_missing?(_name, _include_private) + true + end + + def method_missing(*args, &block) + class_sym = name.to_sym + @@blocks[class_sym] ||= { before: {}, after: {} } + m = args.shift + parts = m.to_s.split('_') + type = parts.shift.to_s.to_sym + method = parts.empty? ? nil : parts.join('_').to_sym + raise NoMethodError, "Undefined method #{m} for class #{self}" unless @@blocks[class_sym][type] && block + + if method + @@blocks[class_sym][type][method] ||= [] + @@blocks[class_sym][type][method] << block + else + @@blocks[class_sym][type][:__all__] ||= [] + @@blocks[class_sym][type][:__all__] << block + end + end + end + + include Dav4rack::HttpStatus + + # path:: Internal resource path (unescaped PATH_INFO) + # request:: Rack::Request + # options:: Any options provided for this resource + # Creates a new instance of the resource. + # NOTE: path and public_path will only differ if the root_uri has been set for the resource. The + # controller will strip out the starting path so the resource can easily determine what + # it is working on. For example: + # request -> /my/webdav/directory/actual/path + # public_path -> /my/webdav/directory/actual/path + # path -> /actual/path + # NOTE: Customized Resources should not use initialize for setup. Instead + # use the #setup method + def initialize(path, request, response, options) + raise ArgumentError, 'path must be present and start with a /' if path.blank? || path[0] != '/' + + @path = path + @propstat_relative_path = !options[:propstat_relative_path].nil? + @root_xml_attributes = options.delete(:root_xml_attributes) || {} + @namespaces = (options[:namespaces] || {}).merge({ DAV_NAMESPACE => DAV_NAMESPACE_NAME }) + @request = request + @response = response + if options.key?(:lock_class) + @lock_class = options[:lock_class] + unless @lock_class.nil? || defined?(@lock_class) + raise NameError, "Unknown lock type constant provided: #{@lock_class}" + end + else + @lock_class = LockStore + end + @options = options + @max_timeout = options[:max_timeout] || 86_400 + @default_timeout = options[:default_timeout] || 60 + @user = @options[:user] || request.ip + setup + end + + # returns a new instance for the given path + def new_for_path(path) + self.class.new path, request, response, @options.merge(user: @user, namespaces: @namespaces) + end + + # override to implement custom authentication + # should return true for successful authentication, false otherwise + def authenticate?(_username, _password) + true + end + + def authentication_error_message + 'Not Authorized' + end + + def authentication_realm + 'Locked content' + end + + # Returns if resource supports locking + def supports_locking? + false + end + + # Returns supported lock types (an array of [lockscope, locktype] pairs) + # i.e. [%w(D:exclusive D:write)] + def supported_locks + [] + end + + # If this is a collection, return the child resources. + def children + NotImplemented + end + + # Is this resource a collection? + def collection? + NotImplemented + end + + # Does this resource exist? + def exist? + NotImplemented + end + + # Return the creation time. + def creation_date + raise NotImplemented + end + + # Return the time of last modification. + def last_modified + raise NotImplemented + end + + # Set the time of last modification. + def last_modified=(_time) + # Is this correct? + raise NotImplemented + end + + # Return an Etag, an unique hash value for this resource. + def etag + raise NotImplemented + end + + # Return the resource type. Generally only used to specify + # resource is a collection. + def resource_type + :collection if collection? + end + + # Return the mime type of this resource. + def content_type + raise NotImplemented + end + + # Return the size in bytes for this resource. + def content_length + raise NotImplemented + end + + # HTTP OPTIONS request. + # resources should override this to set the Allow header to indicate the + # allowed methods. By default, all WebDAV methods are advertised on all + # resources. + def options(_request, _response) + OK + end + + # HTTP GET request. + # + # Write the content of the resource to the response.body. + def get(_request, _response) + NotImplemented + end + + # HTTP HEAD request. + # + # Like GET, but without content. Override if you set custom headers in GET + # to set them here as well. + def head(_request, _response) + OK + end + + # HTTP PUT request. + # + # Save the content of the request.body. + def put(_request) + NotImplemented + end + + # HTTP POST request. + # + # Usually forbidden. + def post(_request, _response) + NotImplemented + end + + # HTTP DELETE request. + # + # Delete this resource. + def delete + NotImplemented + end + + # HTTP COPY request. + # + # Copy this resource to given destination path. + def copy(_dest_path) + NotImplemented + end + + # HTTP MOVE request. + # + # Move this resource to given destination path. + def move(_dest_path) + NotImplemented + end + + # args:: Hash of lock arguments + # Request for a lock on the given resource. A valid lock should lock + # all descendents. Failures should be noted and returned as an exception + # using LockFailure. + # Valid args keys: :timeout -> requested timeout + # :depth -> lock depth + # :scope -> lock scope + # :type -> lock type + # :owner -> lock owner + # Should return a tuple: [lock_time, locktoken] where lock_time is the + # given timeout + # NOTE: See section 9.10 of RFC 4918 for guidance about + # how locks should be generated and the expected responses + # (http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10) + + def lock(args) + raise NotImplemented unless @lock_class + raise Conflict unless parent.exist? + + lock_check args[:scope] + lock = @lock_class.explicit_locks(@path) + .find { |l| l.scope == args[:scope] && l.kind == args[:type] && l.user == @user } + unless lock + token = UUIDTools::UUID.random_create.to_s + lock = @lock_class.generate(@path, @user, token) + lock.scope = args[:scope] + lock.kind = args[:type] + lock.owner = args[:owner] + lock.depth = args[:depth].is_a?(Symbol) ? args[:depth] : args[:depth].to_i + if args[:timeout] + b = args[:timeout] <= @max_timeout && args[:timeout].positive? + lock.timeout = b ? args[:timeout] : @max_timeout + else + lock.timeout = @default_timeout + end + lock.save if lock.respond_to?(:save) + end + begin + lock_check(args[:type]) + rescue Dav4rack::LockFailure => e + lock.destroy + raise e + rescue HttpStatus::Status => e + e + end + [lock.remaining_timeout, lock.token] + end + + # lock_scope:: scope of lock + # Check if resource is locked. Raise Dav4rack::LockFailure if locks are in place. + def lock_check(lock_scope = nil) + return unless @lock_class + + if @lock_class.explicitly_locked?(@path) + if @lock_class.explicit_locks(@path).count { |l| l.scope == 'exclusive' && l.user != @user }.positive? + raise Locked + end + elsif @lock_class.implicitly_locked?(@path) + if lock_scope.to_s == 'exclusive' + locks = @lock_class.implicit_locks(@path) + failure = Dav4rack::LockFailure.new("Failed to lock: #{@path}") + locks.each do |_lock| + failure.add_failure @path, Locked + end + raise failure + else + locks = @lock_class.implict_locks(@path).find_all { |l| l.scope == 'exclusive' && l.user != @user } + unless locks.empty? + failure = LockFailure.new("Failed to lock: #{@path}") + locks.each do |_lock| + failure.add_failure @path, Locked + end + raise failure + end + end + end + end + + # token:: Lock token + # Remove the given lock + def unlock(token) + return NotImplemented unless @lock_class + + token = token.slice(1, token.length - 2) + if token.blank? + BadRequest + else + lock = @lock_class.find_by_token(token) + if lock.nil? || lock.user != @user + Forbidden + elsif !lock.path.match?(/^#{Regexp.escape(@path)}.*$/) + Conflict + else + lock.destroy + NoContent + end + end + end + + # Create this resource as collection. + def make_collection + NotImplemented + end + + # other:: Resource + # Returns if current resource is equal to other resource + def ==(other) + path == other.path + end + + # Name of the resource + def name + ::File.basename path + end + + # Name of the resource to be displayed to the client + def display_name + name + end + + # Available properties + # + # These are returned by PROPFIND without body, or with an allprop body. + DAV_PROPERTIES = %w[ + getetag + resourcetype + getcontenttype + getcontentlength + getlastmodified + creationdate + displayname + ].map { |prop| { name: prop, ns_href: DAV_NAMESPACE } }.freeze + + def properties + props = DAV_PROPERTIES + if supports_locking? + props = props.dup # do not attempt to modify the (frozen) constant + props << { name: 'supportedlock', ns_href: DAV_NAMESPACE } + end + props + end + + # Properties to be returned for PROPFIND + # + # this should include the names of all properties defined on the resource + def propname_properties + props = properties + if supports_locking? + props = props.dup if props.frozen? + props << { name: 'lockdiscovery', ns_href: DAV_NAMESPACE } + end + props + end + + # name:: String - Property name + # Returns the value of the given property + def get_property(element) + return NotFound if element[:ns_href] != DAV_NAMESPACE + + case element[:name] + when 'resourcetype' + resource_type + when 'displayname' + display_name + when 'creationdate' + use_ms_compat_creationdate? ? creation_date.httpdate : creation_date.xmlschema + when 'getcontentlength' + content_length.to_s + when 'getcontenttype' + content_type + when 'getetag' + etag + when 'getlastmodified' + last_modified.httpdate + when 'supportedlock' + supported_locks_xml + when 'lockdiscovery' + lockdiscovery_xml + else + NotImplemented + end + end + + # name:: String - Property name + # value:: New value + # Set the property to the given value + # + # This default implementation does not allow any properties to be changed. + def set_property(_element, _value) + Forbidden + end + + # name:: Property name + # Remove the property from the resource + def remove_property(_element) + Forbidden + end + + # name:: Name of child + # Create a new child with the given name + # NOTE:: Include trailing '/' if child is collection + def child(name) + new_path = @path.dup + new_path << '/' unless new_path[-1] == '/' + new_path << name + new_for_path new_path + end + + # Return parent of this resource + def parent + return nil if @path == '/' + + new_for_path(::File.split(@path).first) unless @path.to_s.empty? + end + + # Return list of descendants + def descendants + list = [] + children.each do |child| + list << child + list.concat child.descendants + end + list + end + + # Index page template for GETs on collection + def index_page + ' %s + +

          %s


          + + %s
          NameSize TypeLast Modified

          ' + end + + def properties_xml_with_depth(process_properties, depth = request.depth) + xml_with_depth(self, depth) do |element, ox_doc| + ox_doc << element.properties_xml(process_properties) + end + end + + def propnames_xml_with_depth(depth = request.depth) + xml_with_depth(self, depth) do |element, ox_doc| + ox_doc << element.propnames_xml + end + end + + # Returns a complete URL for this resource. + # If the propstat_relative_path option is set, just an absolute path will + # be returned. + # If this is a collection, the result will end with a '/' + def href + @href ||= build_href(path, collection: collection?) + end + + # Returns a complete URL for the given path. + # + # If the propstat_relative_path option is set, just an absolute path will + # be returned. If the :collection argument is true, the returned path will + # end with a '/' + def build_href(path, collection: false) + if propstat_relative_path + request.path_for path, collection: collection + else + request.url_for path, collection: collection + end + end + + def propnames_xml + response = Ox::Element.new(D_RESPONSE) + response << ox_element(D_HREF, href) + propstats response, { OK => propname_properties.index_with { |p| [p, nil] } } + response + end + + def properties_xml(process_properties) + response = Ox::Element.new(D_RESPONSE) + response << ox_element(D_HREF, href) + process_properties.each do |type, properties| + propstats response, send(:"#{type}_properties_with_status", properties) + end + response + end + + def supported_locks_xml + supported_locks.map do |scope, type| + ox_lockentry scope, type + end + end + + # array of lock info hashes + # required keys are :time, :token, :depth + # other valid keys are :scope, :type, :root and :owner + def lockdiscovery + [] + end + + # returns an array of activelock ox elements + def lockdiscovery_xml + return unless supports_locking? + + lockdiscovery.map do |lock| + ox_activelock(**lock) + end + end + + def get_properties_with_status(properties) + stats = Hash.new { |h, k| h[k] = [] } + properties.each do |property| + val = get_property(property[:element]) + if val.is_a?(Class) + stats[val] << property[:element] + else + stats[OK] << [property[:element], val] + end + end + stats + end + + def set_properties_with_status(properties) + stats = Hash.new { |h, k| h[k] = [] } + properties.each do |property| + val = set_property(property[:element], property[:value]) + if val.is_a?(Class) + stats[val] << property[:element] + else + stats[OK] << [property[:element], val] + end + end + stats + end + + # resource:: Resource + # elements:: Property hashes (name, namespace, children) + # Removes the given properties from a resource + def remove_properties_with_status(properties) + stats = Hash.new { |h, k| h[k] = [] } + properties.each do |property| + val = remove_property(property[:element]) + if val.is_a?(Class) + stats[val] << property[:element] + else + stats[OK] << [property[:element], val] + end + end + stats + end + + # adds the given xml namespace to namespaces and returns the prefix + def add_namespace(namespace, prefix = "unknown#{rand 65_536}") + return nil if namespace.blank? + return if namespaces.key?(namespace) + + namespaces[namespace] = prefix + prefix + end + + # returns the prefix for the given namespace, adding it if necessary + def prefix_for(ns_href) + namespaces[ns_href] || add_namespace(ns_href) + end + + # response:: parent Ox::Element + # stats:: Array of stats + # Build propstats response + def propstats(response, stats) + return if stats.empty? + + stats.each do |status, props| + propstat = Ox::Element.new(D_PROPSTAT) + prop = Ox::Element.new(D_PROP) + props.each do |element, value| + name = element[:name] + prefix = prefix_for(element[:ns_href]) + name = "#{prefix}:#{name}" if prefix + prop_element = Ox::Element.new(name) + ox_append prop_element, value, prefix: prefix + prop << prop_element + end + propstat << prop + propstat << ox_element(D_STATUS, "#{http_version} #{status.status_line}") + response << propstat + end + end + + def use_compat_mkcol_response? + @options[:compat_mkcol] || @options[:compat_all] + end + + # Returns true if using an MS client + def use_ms_compat_creationdate? + request.ms_client? if @options[:compat_ms_mangled_creationdate] || @options[:compat_all] + end + + # Callback function that adds additional properties to the propfind REQUEST + # These properties will then be parsed and processed as though they were sent + # by the client. This makes sure we can add whatever property we want + # to the response and make it look like the client asked for them. + def propfind_add_additional_properties(properties) + # Default implementation doesn't need to add anything + properties + end + + private + + # Override in child classes for custom setup + def setup + # Nothing to do + end + end +end diff --git a/lib/dav4rack/security_utils.rb b/lib/dav4rack/security_utils.rb new file mode 100644 index 00000000..81646dde --- /dev/null +++ b/lib/dav4rack/security_utils.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Dav4rack + # Implements secure string comparison methods. + # Taken straight from ActiveSupport + module SecurityUtils + def secure_compare(avar, bvar) + return false unless avar.bytesize == bvar.bytesize + + l = avar.unpack "C#{avar.bytesize}" + + res = 0 + bvar.each_byte { |byte| res |= byte ^ l.shift } + res.zero? + end + + module_function :secure_compare + + def variable_size_secure_compare(avar, bvar) + secure_compare(::Digest::SHA256.hexdigest(avar), ::Digest::SHA256.hexdigest(bvar)) + end + + module_function :variable_size_secure_compare + end +end diff --git a/lib/dav4rack/uri.rb b/lib/dav4rack/uri.rb new file mode 100644 index 00000000..2090a558 --- /dev/null +++ b/lib/dav4rack/uri.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'addressable/uri' +require 'English' + +module Dav4rack + # adds a bit of parsing logic around a header URI or path value + class Uri + attr_reader :host, :path, :path_info, :script_name + + def initialize(uri_or_path, script_name: nil) + # More than one leading slash confuses Addressable::URI, resulting e.g. + # with //remote.php/dav/files in a path of /dav/files with a host + # remote.php. + @uri_or_path = uri_or_path.to_s.strip.sub(%r{\A/+}, '/') + @script_name = script_name + parse + end + + def to_s + @uri_or_path + end + + private + + def parse + uri = Addressable::URI.parse @uri_or_path + + @host = uri.host + @path = Addressable::URI.unencode uri.path + + if @script_name + if @path =~ %r{\A(?#{Regexp.escape @script_name}(?/.*))\z} + @path_info = $LAST_MATCH_INFO[:path_info] + end + else + @path_info = @path + end + end + end +end diff --git a/lib/dav4rack/utils.rb b/lib/dav4rack/utils.rb new file mode 100644 index 00000000..ea405b28 --- /dev/null +++ b/lib/dav4rack/utils.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'ostruct' + +module Dav4rack + # Simple wrapper for formatted elements + class DAVElement < OpenStruct + def [](key) + send(key) + end + end + + # Utils + module Utils + DEFAULT_HTTP_VERSION = 'HTTP/1.1' + + def to_element_hash(element) + ns = element.namespace + DAVElement.new( + namespace: ns, + name: element.name, + ns_href: ns&.href, + children: element.children.filter_map { |e| to_element_hash(e) if e.element? }, + attributes: attributes_hash(element) + ) + end + + def to_element_key(element) + ns = element.namespace + "#{ns&.href}!!#{element.name}" + end + + def http_version + DEFAULT_HTTP_VERSION + end + + private + + def attributes_hash(node) + node.attributes.each_with_object({}) do |(_key, attr), ret| + ret[attr.name] = attr.value + end + end + end +end diff --git a/lib/dav4rack/xml_elements.rb b/lib/dav4rack/xml_elements.rb new file mode 100644 index 00000000..5e123652 --- /dev/null +++ b/lib/dav4rack/xml_elements.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module Dav4rack + # XML element + module XmlElements + DAV_NAMESPACE = 'DAV:' + DAV_NAMESPACE_NAME = 'd' + DAV_XML_NS = 'xmlns:d' + XML_VERSION = '1.0' + XML_CONTENT_TYPE = 'application/xml; charset=utf-8' + + %w[ + activelock + depth + error + href + lockdiscovery + lockentry + lockroot + lockscope + locktoken + lock-token-submitted + locktype + multistatus + owner + prop + propstat + response + status + timeout + ].each do |name| + const_set "D_#{name.upcase.tr('-', '_')}", "#{DAV_NAMESPACE_NAME}:#{name}" + end + + INFINITY = 'infinity' + ZERO = '0' + + def ox_element(name, content = nil) + e = Ox::Element.new(name) + e << content if content + e + end + + def ox_append(element, value, prefix: DAV_NAMESPACE_NAME) + case value + when Ox::Element + element << value + when Symbol + element << Ox::Element.new("#{prefix}:#{value}") + when Enumerable + value.each { |v| ox_append element, v, prefix: prefix } + else + element << value.to_s if value + end + end + + def ox_lockentry(scope, type) + Ox::Element.new(D_LOCKENTRY).tap do |e| + e << ox_element(D_LOCKSCOPE, Ox::Element.new(scope)) + e << ox_element(D_LOCKTYPE, Ox::Element.new(type)) + end + end + + def ox_response(path, status) + Ox::Element.new(D_RESPONSE).tap do |e| + # path = "#{scheme}://#{host}:#{port}#{URI.escape(path)}" + e << ox_element(D_HREF, path) + e << ox_element(D_STATUS, "#{http_version} #{status.status_line}") + end + end + + # returns an activelock Ox::Element for the given lock data + def ox_activelock(time:, token:, depth:, scope: nil, type: nil, owner: nil, root: nil) + Ox::Element.new(D_ACTIVELOCK).tap do |activelock| + activelock << ox_element(D_LOCKSCOPE, scope) if scope + activelock << ox_element(D_LOCKTYPE, type) if type + activelock << ox_element(D_DEPTH, depth) + activelock << ox_element(D_TIMEOUT, time ? "Second-#{time}" : INFINITY) + token = ox_element(D_HREF, token) + activelock << ox_element(D_LOCKTOKEN, token) + activelock << ox_element(D_OWNER, owner) if owner + if root + root = ox_element(D_HREF, root) + activelock << ox_element(D_LOCKROOT, root) + end + end + end + + # block is called for each element (at least self, depending on depth also + # with children / further descendants) + def xml_with_depth(resource, depth, &_block) + partial_document = Ox::Document.new + + yield resource, partial_document + + case depth + when 0 + # Nothing to do + when 1 + resource.children.each do |child| + yield child, partial_document + end + else + resource.descendants.each do |desc| + yield desc, partial_document + end + end + + Ox.dump partial_document, { indent: -1 } + end + end +end diff --git a/lib/dav4rack/xml_response.rb b/lib/dav4rack/xml_response.rb new file mode 100644 index 00000000..1d01cac4 --- /dev/null +++ b/lib/dav4rack/xml_response.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Dav4rack + # XML response + class XmlResponse + include XmlElements + + def initialize(response, namespaces, http_version: 'HTTP/1.1') + @response = response + @namespaces = namespaces + @http_version = http_version + end + + def render_xml(xml_body) + @namespaces.each do |href, prefix| + xml_body["xmlns:#{prefix}"] = href + end + + xml_doc = Ox::Document.new(version: '1.0') + xml_doc << xml_body + + @response.body = Ox.dump(xml_doc, { indent: -1, with_xml: true }) + + @response['Content-Type'] = 'application/xml; charset=utf-8' + @response['Content-Length'] = @response.body.bytesize.to_s + end + + def multistatus + multistatus = Ox::Element.new(D_MULTISTATUS) + yield multistatus + render_xml multistatus + end + + def render_failed_precondition(status, href) + error = Ox::Element.new(D_ERROR) + case status.code + when 423 + l = Ox::Element.new(D_LOCK_TOKEN_SUBMITTED) + l << ox_element(D_HREF, href) + error << l + end + render_xml error + end + + def render_lock_errors(errors) + multistatus do |xml| + errors.each do |href, status| + r = response href, status + r << ox_element(D_ERROR, Ox::Element.new(D_LOCK_TOKEN_SUBMITTED)) if status.code == 423 + xml << r + end + end + end + + def render_lockdiscovery(...) + render_xml ox_element(D_PROP, ox_element(D_LOCKDISCOVERY, activelock(...))) + end + + # + # helpers for creating single elements + # + + def response(href, status) + r = Ox::Element.new(D_RESPONSE) + r << ox_element(D_HREF, href) + r << self.status(status) + r + end + + def raw(xml) + Ox::Raw.new xml + end + + def status(status) + ox_element D_STATUS, "#{@http_version} #{status.status_line}" + end + + private + + def activelock(time:, token:, depth:, scope: nil, type: nil, owner: nil, root: nil) + Ox::Element.new(D_ACTIVELOCK).tap do |activelock| + if scope + value = Ox::Element.new("#{DAV_NAMESPACE_NAME}:#{scope}") + activelock << ox_element(D_LOCKSCOPE, value) + end + if type + value = Ox::Element.new("#{DAV_NAMESPACE_NAME}:#{type}") + activelock << ox_element(D_LOCKTYPE, value) + end + activelock << ox_element(D_DEPTH, depth) + activelock << ox_element(D_TIMEOUT, (time ? "Second-#{time}" : INFINITY)) + value = ox_element(D_HREF, token) + activelock << ox_element(D_LOCKTOKEN, value) + activelock << ox_element(D_OWNER, owner) if owner + if root + value = ox_element(D_HREF, root) + activelock << ox_element(D_LOCKROOT, value) + end + end + end + end +end diff --git a/lib/redmine_dmsf.rb b/lib/redmine_dmsf.rb new file mode 100644 index 00000000..07e91390 --- /dev/null +++ b/lib/redmine_dmsf.rb @@ -0,0 +1,274 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +# Main module +module RedmineDmsf + # Settings + class << self + def dmsf_max_file_download + Setting.plugin_redmine_dmsf['dmsf_max_file_download'].to_i + end + + def dmsf_max_email_filesize + Setting.plugin_redmine_dmsf['dmsf_max_email_filesize'].to_i + end + + def dmsf_storage_directory + if Setting.plugin_redmine_dmsf['dmsf_storage_directory'].present? + Setting.plugin_redmine_dmsf['dmsf_storage_directory'].strip + else + 'files/dmsf' + end + 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 + end + + def dmsf_stemming_lang + if Setting.plugin_redmine_dmsf['dmsf_stemming_lang'].present? + Setting.plugin_redmine_dmsf['dmsf_stemming_lang'].strip + else + 'english' + end + end + + def dmsf_stemming_strategy + if Setting.plugin_redmine_dmsf['dmsf_stemming_strategy'].present? + Setting.plugin_redmine_dmsf['dmsf_stemming_strategy'].strip + else + 'STEM_NONE' + end + end + + def dmsf_webdav_strategy + if Setting.plugin_redmine_dmsf['dmsf_webdav_strategy'].present? + Setting.plugin_redmine_dmsf['dmsf_webdav_strategy'].strip + else + 'WEBDAV_READ_ONLY' + end + end + + def dmsf_webdav? + value = Setting.plugin_redmine_dmsf['dmsf_webdav'] + value.to_i.positive? || value == 'true' + end + + def dmsf_display_notified_recipients? + value = Setting.plugin_redmine_dmsf['dmsf_display_notified_recipients'] + value.to_i.positive? || value == 'true' + end + + def dmsf_global_title_format + if Setting.plugin_redmine_dmsf['dmsf_global_title_format'].present? + Setting.plugin_redmine_dmsf['dmsf_global_title_format'].strip + else + '' + end + end + + def dmsf_columns + Setting.plugin_redmine_dmsf['dmsf_columns'].presence || DmsfFolder::DEFAULT_COLUMNS + end + + def dmsf_webdav_ignore + if Setting.plugin_redmine_dmsf['dmsf_webdav_ignore'].present? + Setting.plugin_redmine_dmsf['dmsf_webdav_ignore'].strip + else + '^(\._|\.DS_Store$|Thumbs.db$)' + end + end + + def dmsf_webdav_disable_versioning + if Setting.plugin_redmine_dmsf['dmsf_webdav_disable_versioning'].present? + Setting.plugin_redmine_dmsf['dmsf_webdav_disable_versioning'].strip + else + '^\~\$|\.tmp$' + end + end + + def dmsf_keep_documents_locked? + value = Setting.plugin_redmine_dmsf['dmsf_keep_documents_locked'] + value.to_i.positive? || value == 'true' + end + + def dmsf_act_as_attachable? + value = Setting.plugin_redmine_dmsf['dmsf_act_as_attachable'] + value.to_i.positive? || value == 'true' + end + + def dmsf_documents_email_from + if Setting.plugin_redmine_dmsf['dmsf_documents_email_from'].present? + Setting.plugin_redmine_dmsf['dmsf_documents_email_from'].strip + else + "#{User.current.name} <#{User.current.mail}>" + end + end + + def dmsf_documents_email_reply_to + if Setting.plugin_redmine_dmsf['dmsf_documents_email_reply_to'].present? + Setting.plugin_redmine_dmsf['dmsf_documents_email_reply_to'].strip + else + '' + end + end + + def dmsf_documents_email_links_only? + value = Setting.plugin_redmine_dmsf['dmsf_documents_email_links_only'] + value.to_i.positive? || value == 'true' + end + + def dmsf_enable_cjk_ngrams? + value = Setting.plugin_redmine_dmsf['dmsf_enable_cjk_ngrams'] + value.to_i.positive? || value == 'true' + end + + def dmsf_webdav_use_project_names? + value = Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] + value.to_i.positive? || value == 'true' + end + + def dmsf_webdav_ignore_1b_file_for_authentication? + value = Setting.plugin_redmine_dmsf['dmsf_webdav_ignore_1b_file_for_authentication'] + value.to_i.positive? || value == 'true' + end + + def dmsf_projects_as_subfolders? + value = Setting.plugin_redmine_dmsf['dmsf_projects_as_subfolders'] + value.to_i.positive? || value == 'true' + end + + def only_approval_zero_minor_version? + value = Setting.plugin_redmine_dmsf['only_approval_zero_minor_version'] + value.to_i.positive? || value == 'true' + end + + def dmsf_max_notification_receivers_info + Setting.plugin_redmine_dmsf['dmsf_max_notification_receivers_info'].to_i + end + + def office_bin + if Setting.plugin_redmine_dmsf['office_bin'].present? + Setting.plugin_redmine_dmsf['office_bin'].strip + else + '' + end + end + + def dmsf_global_menu_disabled? + value = Setting.plugin_redmine_dmsf['dmsf_global_menu_disabled'] + value.to_i.positive? || value == 'true' + end + + def dmsf_default_query + Setting.plugin_redmine_dmsf['dmsf_default_query'].to_i + end + + def empty_minor_version_by_default? + value = Setting.plugin_redmine_dmsf['empty_minor_version_by_default'] + value.to_i.positive? || value == 'true' + end + + def physical_file_delete? + value = Setting.plugin_redmine_dmsf['dmsf_really_delete_files'] + value.to_i.positive? || value == 'true' + end + + def remove_original_documents_module? + value = Setting.plugin_redmine_dmsf['remove_original_documents_module'] + value.to_i.positive? || value == 'true' + end + + def dmsf_webdav_authentication + if Setting.plugin_redmine_dmsf['dmsf_webdav_authentication'].present? + Setting.plugin_redmine_dmsf['dmsf_webdav_authentication'].strip + else + 'Digest' + end + end + + def dmsf_default_notifications? + value = Setting.plugin_redmine_dmsf['dmsf_default_notifications'] + value.to_i.positive? || value == 'true' + end + end +end + +# DMSF libraries + +# Validators +require "#{File.dirname(__FILE__)}/../app/validators/dmsf_file_name_validator" +require "#{File.dirname(__FILE__)}/../app/validators/dmsf_max_file_size_validator" +require "#{File.dirname(__FILE__)}/../app/validators/dmsf_workflow_name_validator" +require "#{File.dirname(__FILE__)}/../app/validators/dmsf_url_validator" +require "#{File.dirname(__FILE__)}/../app/validators/dmsf_folder_parent_validator" + +# Patches +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/formatting_helper_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/projects_helper_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/project_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/user_preference_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/user_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/issue_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/role_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/queries_controller_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/pdf_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/access_control_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/search_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/custom_field_patch" +require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/puma_patch" +# A workaround for obsolete 'alias_method' usage in RedmineUp's plugins +if RedmineDmsf::Plugin.an_obsolete_plugin_present? + require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/notifiable_ru_patch" +else + require "#{File.dirname(__FILE__)}/redmine_dmsf/patches/notifiable_patch" +end + +# Load up classes that make up our WebDAV solution ontop of Dav4rack +require "#{File.dirname(__FILE__)}/dav4rack" +require "#{File.dirname(__FILE__)}/redmine_dmsf/webdav/custom_middleware" +require "#{File.dirname(__FILE__)}/redmine_dmsf/webdav/base_resource" +require "#{File.dirname(__FILE__)}/redmine_dmsf/webdav/dmsf_resource" +require "#{File.dirname(__FILE__)}/redmine_dmsf/webdav/index_resource" +require "#{File.dirname(__FILE__)}/redmine_dmsf/webdav/project_resource" +require "#{File.dirname(__FILE__)}/redmine_dmsf/webdav/resource_proxy" + +# Hooks +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/controllers/account_controller_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/controllers/issues_controller_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/controllers/search_controller_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/views/view_projects_form_hook" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/views/base_view_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/views/custom_field_view_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/views/issue_view_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/views/mailer_view_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/views/my_account_view_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/views/search_view_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/helpers/issues_helper_hooks" +require "#{File.dirname(__FILE__)}/redmine_dmsf/hooks/helpers/project_helper_hooks" + +# Macros +require "#{File.dirname(__FILE__)}/redmine_dmsf/macros" + +# Field formats +require "#{File.dirname(__FILE__)}/redmine_dmsf/field_formats/dmsf_file_revision_format" diff --git a/lib/redmine_dmsf/dmsf_zip.rb b/lib/redmine_dmsf/dmsf_zip.rb new file mode 100644 index 00000000..5083f5be --- /dev/null +++ b/lib/redmine_dmsf/dmsf_zip.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +require 'zip' + +module RedmineDmsf + module DmsfZip + FILE_PREFIX = 'dmsf_zip_' + + # ZIP + class Zip + attr_reader :dmsf_files + + delegate :close, to: :@zip_file + + def initialize + @temp_file = Tempfile.new([FILE_PREFIX, '.zip'], Rails.root.join('tmp')) + @zip_file = ::Zip::OutputStream.open(@temp_file) + @files = [] + @dmsf_files = [] + @folders = [] + end + + def read + File.read @temp_file + end + + def finish + @zip_file.close + @temp_file.path + 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) + + if path + string_path = path + else + string_path = dmsf_file.dmsf_folder.nil? ? '' : (dmsf_file.dmsf_folder.dmsf_path_str + File::SEPARATOR) + string_path = string_path[(root_path.length + 1)..string_path.length] if root_path + end + string_path += dmsf_file.formatted_name(member) + + return if @files.include?(string_path) + + 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| + while (buffer = f.read(8192)) + @zip_file.write buffer + end + end + @files << string_path + @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 + zip_entry = ::Zip::Entry.new(@zip_file, string_path, nil, nil, nil, nil, nil, nil, + ::Zip::DOSTime.at(dmsf_folder.modified)) + return if @folders.include?(string_path) + + @zip_file.put_next_entry zip_entry + @folders << string_path + dmsf_folder.dmsf_folders.visible.each { |folder| add_dmsf_folder(folder, member, root_path) } + dmsf_folder.dmsf_files.visible.each { |file| add_dmsf_file(file, member, root_path) } + end + end + end +end diff --git a/lib/redmine_dmsf/field_formats/dmsf_file_revision_format.rb b/lib/redmine_dmsf/field_formats/dmsf_file_revision_format.rb new file mode 100644 index 00000000..8048b024 --- /dev/null +++ b/lib/redmine_dmsf/field_formats/dmsf_file_revision_format.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module FieldFormats + # Custom field type DmsfFileRevision + class DmsfFileRevisionFormat < Redmine::FieldFormat::Unbounded + add 'dmsf_file_revision' + + self.customized_class_names = nil + self.multiple_supported = false + self.bulk_edit_supported = false + + def edit_tag(view, tag_id, tag_name, custom_value, options = {}) + member = Member.find_by(user_id: User.current.id, project_id: custom_value.customized.project.id) + if member&.dmsf_fast_links? + view.text_field_tag(tag_name, custom_value.value, options.merge(id: tag_id)) + else + select_edit_tag(view, tag_id, tag_name, custom_value, options) + end + end + + def select_edit_tag(view, tag_id, tag_name, custom_value, options = {}) + blank_option = ''.html_safe + if custom_value.custom_field.is_required? + if custom_value.custom_field.default_value.blank? + blank_option = + view.content_tag( + 'option', + "--- #{l(:actionview_instancetag_blank_option)} ---", + value: '' + ) + end + else + blank_option = view.content_tag('option', ' '.html_safe, value: '') + end + options_tags = + blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value) + view.select_tag(tag_name, + options_tags, + options.merge(id: tag_id, multiple: custom_value.custom_field.multiple?)) + end + + def possible_values_options(_custom_field, object = nil) + options = [] + if object&.project + files = object.project.dmsf_files.visible.to_a + DmsfFolder.visible(false).where(project_id: object.project.id).find_each do |f| + files += f.dmsf_files.visible.to_a + end + files.sort! { |a, b| a.title.casecmp(b.title) } + options = files.map { |f| [f.name, f.last_revision.id.to_s] } + end + options + end + + def formatted_value(view, _custom_field, value, _customized = nil, _html = false) + return '' if value.blank? + + revision = DmsfFileRevision.find_by(id: value) + unless revision && + User.current.allowed_to?(:view_dmsf_files, revision.dmsf_file.project) && + (revision.dmsf_file.dmsf_folder.nil? || revision.dmsf_file.dmsf_folder.visible?) + return '' + end + + member = Member.find_by(user_id: User.current.id, project_id: revision.dmsf_file.project.id) + filename = revision.formatted_name(member) + file_view_url = view.static_dmsf_file_path(revision.dmsf_file, download: revision, filename: filename) + icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(revision.dmsf_file.name)) + view.link_to( + sprite_icon(icon_name, h(filename)), + file_view_url, + target: '_blank', + 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}" + ) + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/controllers/account_controller_hooks.rb b/lib/redmine_dmsf/hooks/controllers/account_controller_hooks.rb new file mode 100644 index 00000000..05bb219f --- /dev/null +++ b/lib/redmine_dmsf/hooks/controllers/account_controller_hooks.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Controllers + # Account controller hooks + class AccountControllerHooks < Redmine::Hook::Listener + def controller_account_success_authentication_after(context = {}) + return unless context.is_a?(Hash) + + controller = context[:controller] + return unless controller + + user = context[:user] + return unless user + + return unless RedmineDmsf.dmsf_webdav_authentication == 'Digest' + + # Updates user's DMSF WebDAV digest + if controller.params[:password].present? + token = Token.find_by(user_id: user.id, action: 'dmsf_webdav_digest') + token ||= Token.create!(user_id: user.id, action: 'dmsf_webdav_digest') + token.value = ActiveSupport::Digest.hexdigest( + "#{user.login}:#{RedmineDmsf::Webdav::AUTHENTICATION_REALM}:#{controller.params[:password]}" + ) + token.save + end + rescue StandardError => e + Rails.logger.error e.message + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/controllers/issues_controller_hooks.rb b/lib/redmine_dmsf/hooks/controllers/issues_controller_hooks.rb new file mode 100644 index 00000000..c7da282c --- /dev/null +++ b/lib/redmine_dmsf/hooks/controllers/issues_controller_hooks.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Controllers + # issue controller hooks + class IssuesControllerHooks < Redmine::Hook::Listener + def controller_issues_new_before_save(context = {}) + controller_issues_before_save context + end + + def controller_issues_new_after_save(context = {}) + controller_issues_after_save context + # Copy documents from the source issue + return unless context.is_a?(Hash) + + issue = context[:issue] + params = context[:params] + copied_from = Issue.find_by(id: params[:copy_from]) if params[:copy_from].present? + # Save documents + return unless copied_from + + copied_from.dmsf_files.each do |dmsf_file| + dmsf_file.copy_to issue.project, issue.system_folder(create: true) + end + end + + def controller_issues_edit_before_save(context = {}) + controller_issues_before_save context + end + + def controller_issues_edit_after_save(context = {}) + controller_issues_after_save context, edit: true + end + + def controller_issues_bulk_edit_before_save(context = {}) + controller_issues_before_save context + # Call also the after safe hook, 'cause it's missing in Redmine + controller_issues_after_save(context, edit: true) + end + + private + + def controller_issues_before_save(context) + return unless context.is_a?(Hash) + + issue = context[:issue] + @new_object = issue.new_record? + params = context[:params] + # Save upload preferences DMS/Attachments + User.current.pref.dmsf_attachments_upload_choice = params[:dmsf_attachments_upload_choice] + # Save attachments + issue.save_dmsf_attachments params[:dmsf_attachments] + issue.save_dmsf_links params[:dmsf_links] + issue.save_dmsf_attachments_wfs params[:dmsf_attachments_wfs], params[:dmsf_attachments] + issue.save_dmsf_links_wfs params[:dmsf_links_wfs] + end + + def controller_issues_after_save(context, edit: false) + return unless context.is_a?(Hash) + + issue = context[:issue] + params = context[:params] + controller = context[:controller] + if edit && ((params[:issue] && params[:issue][:project_id].present?) || context[:new_project]) + project_id = context[:new_project] ? context[:new_project].id : params[:issue][:project_id].to_i + old_project_id = context[:project] ? context[:project].id : project_id + # Sync the title with the issue's subject + old_system_folder = issue.system_folder(create: false, prj_id: old_project_id) + if old_system_folder + old_system_folder.title = "#{issue.id} - #{DmsfFolder.get_valid_title(issue.subject)}" + unless old_system_folder.save + controller.flash[:error] = old_system_folder.errors.full_messages.to_sentence + Rails.logger.error old_system_folder.errors.full_messages.to_sentence + end + end + # Move documents, links and folders if needed + if project_id != old_project_id + if old_system_folder + new_main_system_folder = issue.main_system_folder(create: true) + if new_main_system_folder + old_system_folder.dmsf_folder_id = new_main_system_folder.id + old_system_folder.project_id = project_id + unless old_system_folder.save + controller.flash[:error] = old_system_folder.errors.full_messages.to_sentence + Rails.logger.error old_system_folder.errors.full_messages.to_sentence + end + issue.dmsf_files.each do |dmsf_file| + dmsf_file.project_id = project_id + next if dmsf_file.save + + controller.flash[:error] = dmsf_file.errors.full_messages.to_sentence + Rails.logger.error dmsf_file.errors.full_messages.to_sentence + end + issue.dmsf_links.each do |dmsf_link| + dmsf_link.project_id = project_id + next if dmsf_link.save + + controller.flash[:error] = dmsf_link.errors.full_messages.to_sentence + Rails.logger.error dmsf_link.errors.full_messages.to_sentence + end + end + end + + issue.descendants.each do |i| + old_system_folder = i.system_folder(create: false, prj_id: old_project_id) + next unless old_system_folder + + new_main_system_folder = i.main_system_folder(create: true) + if new_main_system_folder + old_system_folder.dmsf_folder_id = new_main_system_folder.id + old_system_folder.project_id = project_id + unless old_system_folder.save + controller.flash[:error] = old_system_folder.errors.full_messages.to_sentence + Rails.logger.error old_system_folder.errors.full_messages.to_sentence + end + i.dmsf_files.each do |dmsf_file| + dmsf_file.project_id = project_id + next if dmsf_file.save + + controller.flash[:error] = dmsf_file.errors.full_messages.to_sentence + Rails.logger.error dmsf_file.errors.full_messages.to_sentence + end + end + i.dmsf_links.each do |dmsf_link| + dmsf_link.project_id = project_id + next if dmsf_link.save + + controller.flash[:error] = dmsf_link.errors.full_messages.to_sentence + Rails.logger.error dmsf_link.errors.full_messages.to_sentence + end + end + end + end + # Attach DMS documents + uploaded_files = params[:dmsf_attachments] + details = params[:committed_files] + if uploaded_files + system_folder = issue.system_folder(create: true) + uploaded_files.each do |key, uploaded_file| + upload = DmsfUpload.create_from_uploaded_attachment(issue.project, system_folder, uploaded_file) + next unless upload + + uploaded_file[:disk_filename] = upload.disk_filename + uploaded_file[:name] = upload.name + uploaded_file[:title] = upload.title + if details + uploaded_file[:description] = details[key][:description] + uploaded_file[:comment] = details[key][:comment] + uploaded_file[:version_major] = details[key][:version_major] + uploaded_file[:version_minor] = details[key][:version_minor] + uploaded_file[:version_patch] = details[key][:version_patch] + else + uploaded_file[:version_major] = 0 + uploaded_file[:version_minor] = 1 + end + 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 + uploaded_file[:custom_field_values] = details[key][:custom_field_values] if details + end + DmsfUploadHelper.commit_files_internal uploaded_files, issue.project, system_folder, context[:controller], + issue, new_object: @new_object + end + # Attach DMS links + issue.saved_dmsf_links.each do |l| + file = l.target_file + revision = file.last_revision + system_folder = issue.system_folder(create: true) + next unless system_folder + + l.project_id = system_folder.project_id + l.dmsf_folder_id = system_folder.id + issue.dmsf_file_added(file) if l.save && !@new_object + wf = issue.saved_dmsf_links_wfs[l.id] + next unless wf + + # Assign the workflow + revision.set_workflow wf.id, 'assign' + revision.assign_workflow wf.id + # Start the workflow + revision.set_workflow wf.id, 'start' + if revision.save + wf.notify_users issue.project, revision, context[:controller] + begin + file.lock! + rescue DmsfLockError => e + Rails.logger.warn e.message + end + else + Rails.logger.error l(:error_workflow_assign) + end + end + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/controllers/search_controller_hooks.rb b/lib/redmine_dmsf/hooks/controllers/search_controller_hooks.rb new file mode 100644 index 00000000..6ca5e758 --- /dev/null +++ b/lib/redmine_dmsf/hooks/controllers/search_controller_hooks.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Controllers + # Search controller hooks + class SearchControllerHooks < Redmine::Hook::Listener + include Rails.application.routes.url_helpers + + def controller_search_quick_jump(context = {}) + return unless context.is_a?(Hash) + + question = context[:question] + return if question.blank? + + return unless question.match(/^D(\d+)$/) && DmsfFile.visible.exists?(id: Regexp.last_match(1)) + + dmsf_file_path id: Regexp.last_match(1) + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/helpers/issues_helper_hooks.rb b/lib/redmine_dmsf/hooks/helpers/issues_helper_hooks.rb new file mode 100644 index 00000000..0db35c6f --- /dev/null +++ b/lib/redmine_dmsf/hooks/helpers/issues_helper_hooks.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Helpers + # Issue helper hooks + class IssuesHelperHooks < Redmine::Hook::Listener + def helper_issues_show_detail_after_setting(context) + return unless context.is_a?(Hash) + + detail = context[:detail] + case detail.property + when 'dmsf_file' + detail.prop_key = l(:label_document) + detail.property = 'attachment' + end + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/helpers/project_helper_hooks.rb b/lib/redmine_dmsf/hooks/helpers/project_helper_hooks.rb new file mode 100644 index 00000000..6cfd54b8 --- /dev/null +++ b/lib/redmine_dmsf/hooks/helpers/project_helper_hooks.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Helpers + # Project helper hooks + class ProjectHelperHooks < Redmine::Hook::Listener + def helper_project_settings_tabs(context) + dmsf_tabs = [ + { name: 'dmsf', + action: { controller: 'dmsf_state', action: 'user_pref_save' }, + partial: 'dmsf_state/user_pref', + label: :menu_dmsf }, + { name: 'dmsf_workflow', + action: { controller: 'dmsf_workflows', action: 'index' }, + partial: 'dmsf_workflows/main', + label: :label_dmsf_workflow_plural } + ] + context[:tabs].concat( + dmsf_tabs.select { |dmsf_tab| User.current.allowed_to?(dmsf_tab[:action], context[:project]) } + ) + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/views/base_view_hooks.rb b/lib/redmine_dmsf/hooks/views/base_view_hooks.rb new file mode 100644 index 00000000..b442f561 --- /dev/null +++ b/lib/redmine_dmsf/hooks/views/base_view_hooks.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Views + # Base view hooks + class BaseViewHooks < Redmine::Hook::ViewListener + def view_layouts_base_html_head(context = {}) + unless /^(Dmsf|Projects|Issues|Queries|MyController|SettingsController|WikiController)/.match?( + context[:controller].class.name + ) + return + end + + context[:controller].send :render_to_string, { partial: 'hooks/redmine_dmsf/view_layouts_base_html_head' } + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/views/custom_field_view_hooks.rb b/lib/redmine_dmsf/hooks/views/custom_field_view_hooks.rb new file mode 100644 index 00000000..23fb19cd --- /dev/null +++ b/lib/redmine_dmsf/hooks/views/custom_field_view_hooks.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Views + # Custom field view hooks + class CustomFieldViewHooks < Redmine::Hook::ViewListener + def view_custom_fields_form_dmsf_file_revision_custom_field(context = {}) + html = +'' + if context.is_a?(Hash) && context[:form] + # Add the inheritable option + f = context[:form] + html = content_tag(:p, f.check_box(:dmsf_not_inheritable)) + # Add is filter option + if context[:custom_field] + custom_field = context[:custom_field] + html << content_tag(:p, f.check_box(:is_filter)) if custom_field.format.is_filter_supported + end + end + html + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/views/issue_view_hooks.rb b/lib/redmine_dmsf/hooks/views/issue_view_hooks.rb new file mode 100644 index 00000000..2d0e8b63 --- /dev/null +++ b/lib/redmine_dmsf/hooks/views/issue_view_hooks.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Views + # Issue view hooks + class IssueViewHooks < Redmine::Hook::ViewListener + include DmsfQueriesHelper + include DmsfFilesHelper + + def view_issues_form_details_bottom(context = {}) + context[:container] = context[:issue] + attach_documents_form(context) + end + + def view_attachments_form_top(context = {}) + html = +'' + container = context[:container] + # Radio buttons + if allowed_to_attach_documents?(container) + html << '

          ' + classes = +'inline' + html << "' + classes << ' dmsf_attachments_label' unless container&.new_record? + html << "' + html << '

          ' + if User.current.pref.dmsf_attachments_upload_choice == 'DMSF' + html << context[:hook_caller].javascript_tag( + "$('.attachments-container:not(.dmsf-uploader)').hide();" + ) + end + end + # Upload form + html << attach_documents_form(context, label: false) if allowed_to_attach_documents?(container) + html + end + + def view_issues_show_description_bottom(context = {}) + show_attached_documents context[:issue], context[:controller] + end + + def view_issues_show_attachments_table_bottom(context = {}) + show_attached_documents context[:container], context[:controller], context[:attachments] + end + + def view_issues_dms_attachments(context = {}) + 'yes' if get_links(context[:container]).any? + end + + def view_issues_show_thumbnails(context = {}) + show_thumbnails(context[:container], context[:controller]) + end + + def view_issues_dms_thumbnails(context = {}) + links = get_links(context[:container]) + return unless links.present? && Setting.thumbnails_enabled? + + images = links.pluck(0).select(&:image?) + 'yes' if images.any? + end + + def view_issues_edit_notes_bottom_style(context = {}) + if User.current.pref.dmsf_attachments_upload_choice == 'Attachments' || + !allowed_to_attach_documents?(context[:container]) + '' + else + 'display: none' + end + end + + private + + def allowed_to_attach_documents?(container) + return false unless container.respond_to?(:project) && container.respond_to?(:saved_dmsf_attachments) && + RedmineDmsf.dmsf_act_as_attachable? + + return false if container.project && (!User.current.allowed_to?(:file_manipulation, container.project) || + (container.project&.dmsf_act_as_attachable != Project::ATTACHABLE_DMS_AND_ATTACHMENTS)) + + true + end + + def get_links(container) + links = [] + if defined?(container.dmsf_files) && + User.current.allowed_to?(:view_dmsf_files, container.project) && + RedmineDmsf.dmsf_act_as_attachable? && + container.project.dmsf_act_as_attachable == Project::ATTACHABLE_DMS_AND_ATTACHMENTS + container.dmsf_files.each do |dmsf_file| + links << [dmsf_file, nil, dmsf_file.created_at] if dmsf_file.last_revision + end + container.dmsf_links.each do |dmsf_link| + dmsf_file = dmsf_link.target_file + links << [dmsf_file, dmsf_link, dmsf_link.created_at] if dmsf_file&.last_revision + end + # Sort by 'create_at' + links.sort_by! { |a| a[2] } + end + links + end + + def show_thumbnails(container, controller) + links = get_links(container) + return if links.blank? + + controller.send :render_to_string, { partial: 'dmsf_files/thumbnails', + locals: { links: links, + thumbnails: Setting.thumbnails_enabled?, + link_to: false } } + end + + def attach_documents_form(context, label: true) + return unless context.is_a?(Hash) && context[:container] + + # Add Dmsf upload form + container = context[:container] + return unless allowed_to_attach_documents?(container) + + html = +'' + if label + html << "" + html << '' + else + html << %() + end + html << context[:controller].send(:render_to_string, { partial: 'dmsf_upload/form', + locals: { container: container, + multiple: true, + awf: true } }) + html << '' + html << '

          ' + html + end + + def show_attached_documents(container, controller, _attachments = nil) + # Add list of attached documents + links = get_links(container) + return if links.blank? + + controller.send :render_to_string, + { partial: 'dmsf_files/links', + locals: { links: links, thumbnails: Setting.thumbnails_enabled? } } + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/views/mailer_view_hooks.rb b/lib/redmine_dmsf/hooks/views/mailer_view_hooks.rb new file mode 100644 index 00000000..2f3e22cc --- /dev/null +++ b/lib/redmine_dmsf/hooks/views/mailer_view_hooks.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# User form view hooks + +module RedmineDmsf + module Hooks + module Views + # Mailer view hooks + class MailerViewHooks < Redmine::Hook::ViewListener + def view_mailer_issue_show_text_bottom(context = {}) + context[:controller].send :render_to_string, + { partial: 'hooks/redmine_dmsf/view_mailer_issue', + locals: { issue: context[:issue] } } + end + + def view_mailer_issue_show_html_bottom(context = {}) + context[:controller].send :render_to_string, + { partial: 'hooks/redmine_dmsf/view_mailer_issue', + locals: { issue: context[:issue] } } + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/views/my_account_view_hooks.rb b/lib/redmine_dmsf/hooks/views/my_account_view_hooks.rb new file mode 100644 index 00000000..403f1110 --- /dev/null +++ b/lib/redmine_dmsf/hooks/views/my_account_view_hooks.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# User form view hooks + +module RedmineDmsf + module Hooks + module Views + # My account view hooks + class MyAccountViewHooks < Redmine::Hook::ViewListener + def view_my_account_preferences(context = {}) + context[:controller].send :render_to_string, { partial: 'hooks/redmine_dmsf/view_my_account' } + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/views/search_view_hooks.rb b/lib/redmine_dmsf/hooks/views/search_view_hooks.rb new file mode 100644 index 00000000..da3fa1d2 --- /dev/null +++ b/lib/redmine_dmsf/hooks/views/search_view_hooks.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Views + # Search view hooks + class SearchViewHooks < Redmine::Hook::ViewListener + def view_search_index_container(context = {}) + return unless context[:object].is_a?(DmsfFile) || context[:object].is_a?(DmsfFolder) + + str = context[:controller].send(:render_to_string, + partial: 'search/container', + locals: { object: context[:object] }) + " #{str} /" if str + end + end + end + end +end diff --git a/lib/redmine_dmsf/hooks/views/view_projects_form_hook.rb b/lib/redmine_dmsf/hooks/views/view_projects_form_hook.rb new file mode 100644 index 00000000..289b27fe --- /dev/null +++ b/lib/redmine_dmsf/hooks/views/view_projects_form_hook.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Hooks + module Views + # Project view hooks + class ViewProjectsFormHook < Redmine::Hook::ViewListener + include Redmine::I18n + + def view_projects_form(context = {}) + context[:controller].send :render_to_string, + { partial: 'hooks/redmine_dmsf/view_projects_form', locals: context } + end + end + end + end +end diff --git a/lib/redmine_dmsf/lockable.rb b/lib/redmine_dmsf/lockable.rb new file mode 100644 index 00000000..a1292c68 --- /dev/null +++ b/lib/redmine_dmsf/lockable.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + # Lockable + module Lockable + def locked? + !lock.empty? + end + + # lock: + # Returns an array with current lock objects that affect the current object + # optional: tree = true (show entire tree?) + def lock(tree: true) + ret = [] + unless locks.empty? + locks.each do |lock| + ret << lock unless lock.expired? + end + end + if tree && dmsf_folder + ret |= dmsf_folder.locks.empty? ? dmsf_folder.lock : dmsf_folder.locks + end + ret + end + + def lock!(scope = :scope_exclusive, type = :type_write, expire = nil, owner = nil) + # Raise a lock error if entity is locked, but its not at resource level + existing = lock(tree: false) + raise DmsfLockError, l(:error_resource_or_parent_locked) if locked? && existing.empty? + + unless existing.empty? + if (existing[0].lock_scope == :scope_shared) && (scope == :scope_shared) + # RFC states if an item is exclusively locked and another lock is attempted we reject + # if the item is shared locked however, we can always add another lock to it + raise DmsfLockError, l(:error_parent_locked) if dmsf_folder.locked? + elsif scope == :scope_exclusive + raise DmsfLockError, l(:error_lock_exclusively) + end + end + l = DmsfLock.new + l.entity_id = id + l.entity_type = is_a?(DmsfFile) ? 0 : 1 + l.lock_type = type + l.lock_scope = scope + l.user = User.current + l.expires_at = expire + l.dmsf_file_last_revision_id = last_revision.id if is_a?(DmsfFile) + l.owner = owner + l.save! + reload # Reload the object being locked in order to contain just created lock when asked + l + end + + def unlockable? + return false unless locked? + + existing = lock(tree: true) + # If it's empty, it's a folder that's locked (not root) + existing.empty? || dmsf_folder&.locked? ? false : true + end + + # By using the path upwards, surely this would be quicker? + def locked_for_user?(args = nil) + return false unless locked? + + shared = nil + dmsf_path.each do |entity| + locks = entity.locks || entity.lock(false) + next if locks.empty? + + locks.each do |lock| + next if lock.expired? # In case we're in between updates + + owner = args[:owner] if args + owner ||= User.current&.login if lock.owner + if lock.lock_scope == :scope_exclusive + return true if (lock.user&.id != User.current.id) || (lock.owner&.downcase != owner&.downcase) + else + shared = true if shared.nil? + if (shared && (lock.user&.id == User.current.id) && (lock.owner&.downcase == owner&.downcase)) || + (args && (args[:scope] == 'shared')) + shared = false + end + end + end + return true if shared + end + false + end + + def unlock!(force_file_unlock_allowed: false, owner: nil) + raise DmsfLockError, l(:warning_file_not_locked) unless locked? + + existing = lock(tree: true) + destroyed = false + # If its empty its a folder that's locked (not root) + if existing.empty? || (!dmsf_folder.nil? && dmsf_folder.locked?) + raise DmsfLockError, l(:error_unlock_parent_locked) + end + + # If entity is locked to you, you aren't the lock originator (or named in a shared lock) so deny action + # Unless of course you have the rights to force an unlock + if locked_for_user? && !User.current.allowed_to?(:force_file_unlock, project) && !force_file_unlock_allowed + raise DmsfLockError, l(:error_only_user_that_locked_file_can_unlock_it) + end + + # Now we need to determine lock type and do the needful + if (existing.count == 1) && (existing[0].lock_scope == :exclusive) + existing[0].destroy + destroyed = true + else + existing.each do |lock| + owner = User.current&.login if lock.owner && owner.nil? + unless ((lock.user&.id == User.current.id) && (lock.owner&.downcase == owner&.downcase)) || + User.current.admin? + next + end + + lock.destroy + destroyed = true + break + end + # At first it was going to be allowed for someone with force_file_unlock to delete all shared by default + # Instead, they by default remove themselves from shared lock, and everyone from shared lock if they're not + # on said lock + if !destroyed && (User.current.allowed_to?(:force_file_unlock, project) || force_file_unlock_allowed) + locks.delete_all + destroyed = true + end + end + + if destroyed + reload + locks.reload + end + destroyed + end + end +end diff --git a/lib/redmine_dmsf/macros.rb b/lib/redmine_dmsf/macros.rb new file mode 100644 index 00000000..55dccadd --- /dev/null +++ b/lib/redmine_dmsf/macros.rb @@ -0,0 +1,301 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + # Macros + module Macros + Redmine::WikiFormatting::Macros.register do + # dmsf - link to a document + desc %{Wiki link to DMSF file: + {{dmsf(file_id [, title [, revision_id]])}} + _file_id_ / _revision_id_ can be found in the link for file/revision download.} + macro :dmsf do |_obj, args| + raise ArgumentError if args.empty? # Requires file id + + file = DmsfFile.visible.find_by(id: args[0]) + return "{{dmsf(#{args[0]})}}" unless file + unless User.current&.allowed_to?(:view_dmsf_files, file.project, { id: file.id }) + raise ::I18n.t(:notice_not_authorized) + end + + if args[2].blank? + revision = file.last_revision + else + revision = DmsfFileRevision.find_by(id: args[2], dmsf_file_id: args[0]) + return "{{dmsf(#{args[0]}, #{args[1]}, #{args[2]})}" unless revision + end + title = args[1].presence || file.title + title.gsub!(/\A"|"\z/, '') # Remove apostrophes + title.gsub!(/\A'|'\z/, '') + title = file.title if title.empty? + url = view_dmsf_file_url(id: file.id, download: args[2]) + link_to h(title), url, + target: '_blank', + rel: 'noopener', + title: h(revision.tooltip), + 'data-downloadurl' => "#{file.last_revision.detect_content_type}:#{h(file.name)}:#{url}" + end + + # dmsff - link to a folder + desc %{Wiki link to DMSF folder: + {{dmsff([folder_id [, title]])}} + _folder_id_ can be found in the link for folder opening. Without arguments return link to main folder + 'Documents'} + macro :dmsff do |_obj, args| + if args.empty? + unless User.current.allowed_to?(:view_dmsf_folders, @project) && @project.module_enabled?(:dmsf) + raise ::I18n.t(:notice_not_authorized) + end + + return link_to ::I18n.t(:link_documents), dmsf_folder_url(@project) + else + folder = DmsfFolder.visible.find_by(id: args[0]) + return "{{dmsff(#{args[0]})}}" unless folder + raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_folders, folder.project) + + title = args[1].presence || folder.title + title.gsub!(/\A"|"\z/, '') # Remove leading and trailing apostrophe + title.gsub!(/\A'|'\z/, '') + title = folder.title if title.empty? + link_to h(title), dmsf_folder_url(folder.project, folder_id: folder) + end + end + + # dmsfd - link to the document's details + desc %{Wiki link to DMSF document details: + {{dmsfd(document_id [, title])}} + _document_id_ can be found in the document's details.} + macro :dmsfd do |_obj, args| + raise ArgumentError if args.empty? # Requires file id + + file = DmsfFile.visible.find_by(id: args[0]) + return "{{dmsfd(#{args[0]})}}" unless file + raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_files, file.project) + + title = args[1].presence || file.title + title.gsub!(/\A"|"\z/, '') # Remove leading and trailing apostrophe + title.gsub!(/\A'|'\z/, '') + link_to h(title), dmsf_file_path(id: file) + end + + # dmsfdesc - text referring to the document's description + desc %{Text referring to DMSF document description: + {{dmsfdesc(document_id)}} + _document_id_ can be found in the document's details.} + macro :dmsfdesc do |_obj, args| + raise ArgumentError if args.empty? # Requires file id + + file = DmsfFile.visible.find_by(id: args[0]) + return "{{dmsfdesc(#{args[0]})}}" unless file + raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_files, file.project) + + textilizable file.description + end + + # dmsfversion - text referring to the document's version + desc %{Text referring to DMSF document version: + {{dmsfversion(document_id [, revision_id])}} + _document_id_ can be found in the document's details.} + macro :dmsfversion do |_obj, args| + raise ArgumentError if args.empty? # Requires file id + + file = DmsfFile.visible.find_by(id: args[0]) + return "{{dmsfversion(#{args[0]})}}" unless file + + unless User.current&.allowed_to?(:view_dmsf_files, file.project, { id: file.id }) + raise ::I18n.t(:notice_not_authorized) + end + + if args[1].blank? + revision = file.last_revision + else + revision = DmsfFileRevision.find_by(id: args[1], dmsf_file_id: args[0]) + return "{{dmsfversion(#{args[0]}, #{args[1]})}}" unless revision + end + revision.version + end + + # dmsflastupdate - text referring to the document's last update date + desc %{Text referring to DMSF document last update date: + {{dmsflastupdate(document_id)}} + _document_id_ can be found in the document's details.} + macro :dmsflastupdate do |_obj, args| + raise ArgumentError if args.empty? # Requires file id + + file = DmsfFile.visible.find_by(id: args[0]) + return "{{dmsflastupdate(#{args[0]})}}" unless file + raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_files, file.project) + + textilizable format_time(file.last_revision.updated_at) + end + + # dmsft - link to the document's content preview + desc %{Text referring to DMSF text document content: + {{dmsft(file_id, lines_count)}} + _file_id_ can be found in the document's details. _lines_count_ indicates quantity of lines to show.} + macro :dmsft do |_obj, args| + raise ArgumentError if args.length < 2 # Requires file id and lines number + + file = DmsfFile.visible.find_by(id: args[0]) + return "{{dmsft(#{args[0]}, #{args[1]})}}" unless file + raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_files, file.project) + + content_tag :pre, file.text_preview(args[1]) + end + + # dmsf_image - link to an image + desc %{Wiki DMSF image: + {{dmsf_image(file_id)}} + {{dmsf_image(file_id1 file_id2 file_id3)}} -- multiple images + {{dmsf_image(file_id, size=50%)}} -- with size 50% + {{dmsf_image(file_id, size=300)}} -- with size 300 + {{dmsf_image(file_id, height=300)}} -- with height (auto width) + {{dmsf_image(file_id, width=300)}} -- with width (auto height) + {{dmsf_image(file_id, size=640x480)}} -- with size 640x480"} + macro :dmsf_image 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] + ids = args[0].split + html = [] + ids.each do |id| + file = DmsfFile.visible.find_by(id: id) + unless file + html << "{{dmsf_image(#{args[0]})}}" + 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? + + 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) + html << if size&.include?('%') + image_tag url, alt: filename, title: file.title, width: size, height: 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, size: size + end + end + safe_join html + end + + # dmsf_video - link to a video + desc %{Wiki DMSF video: + {{dmsf_video(file_id)}}\n" + + {{dmsf_video(file_id, size=50%)}} -- with size 50% + {{dmsf_video(file_id, size=300)}} -- with size 300x300 + {{dmsf_video(file_id, height=300)}} -- with height (auto width) + {{dmsf_video(file_id, width=300)}} -- with width (auto height) + {{dmsf_video(file_id, size=640x480)}} -- with size 640x480} + macro :dmsf_video 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] + file = DmsfFile.visible.find_by(id: args[0]) + return "{{dmsf_video(#{args[0]})}}" unless file + raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_files, file.project) + raise ::I18n.t(:error_not_supported_video_format) unless file.video? + + 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) + if size&.include?('%') + video_tag url, controls: true, alt: filename, title: file.title, width: size, height: size + elsif height + video_tag url, controls: true, alt: filename, title: file.title, width: 'auto', height: height + elsif width + video_tag url, controls: true, alt: filename, title: file.title, width: width, height: 'auto' + else + video_tag url, controls: true, alt: filename, title: file.title, size: size + end + end + + # dmsftn - link to an image thumbnail + desc %{Wiki DMSF thumbnail: + {{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, 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] + ids = args[0].split + html = [] + ids.each do |id| + file = DmsfFile.visible.find_by(id: id) + unless file + html << "{{dmsftn(#{id})}}" + 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? + + 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, + target: '_blank', + rel: 'noopener', + title: h(file.last_revision.try(:tooltip)), + 'data-downloadurl' => "#{file.last_revision.detect_content_type}:#{h(file.name)}:#{url}") + end + safe_join html + end + + # dmsfw - link to a document's approval workflow status + desc %{Text referring to DMSF document's approval workflow status: + {{dmsfw(file_id)}} + _file_id_ can be found in the document's details.} + macro :dmsfw do |_obj, args| + raise ArgumentError if args.empty? # Requires file id + + file = DmsfFile.visible.find_by(id: args[0]) + return "{{dmsfw(#{args[0]})}}" unless file + raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_files, file.project) + + file.last_revision.workflow_str(false) + end + end + end +end diff --git a/lib/redmine_dmsf/patches/access_control_patch.rb b/lib/redmine_dmsf/patches/access_control_patch.rb new file mode 100644 index 00000000..ab301974 --- /dev/null +++ b/lib/redmine_dmsf/patches/access_control_patch.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # AccessControl patch + module AccessControlPatch + ################################################################################################################## + # Overridden methods + def self.prepended(base) + base.singleton_class.prepend(ClassMethods) + end + + # Class methods + module ClassMethods + def available_project_modules + # Removes the original Documents from project's modules (replaced with DMSF) + if RedmineDmsf.remove_original_documents_module? + super.reject { |m| m == :documents } + else + super + end + end + end + end + end +end + +# Apply the patch +Redmine::AccessControl.prepend RedmineDmsf::Patches::AccessControlPatch diff --git a/lib/redmine_dmsf/patches/custom_field_patch.rb b/lib/redmine_dmsf/patches/custom_field_patch.rb new file mode 100644 index 00000000..43223219 --- /dev/null +++ b/lib/redmine_dmsf/patches/custom_field_patch.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # CustomField patch + module CustomFieldPatch + def self.included(base) + base.class_eval do + safe_attributes :dmsf_not_inheritable + end + end + end + end +end + +# Apply patch +CustomField.include RedmineDmsf::Patches::CustomFieldPatch diff --git a/lib/redmine_dmsf/patches/formatting_helper_patch.rb b/lib/redmine_dmsf/patches/formatting_helper_patch.rb new file mode 100644 index 00000000..7b7a403b --- /dev/null +++ b/lib/redmine_dmsf/patches/formatting_helper_patch.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # Formatting helper + module FormattingHelperPatch + def heads_for_wiki_formatter + super + return if @dmsf_macro_list + + @dmsf_macro_list = [] + Redmine::WikiFormatting::Macros.available_macros.each_key do |key| + @dmsf_macro_list << key.to_s if key.to_s.match?(/^dmsf/) + end + # If localized files for the current language are not available, switch to English + lang = current_language.to_s.downcase + path = File.join(File.dirname(__FILE__), + '..', '..', '..', 'assets', 'help', lang, 'wiki_syntax.html') + @dmsf_macro_list << (File.exist?(path) ? "#{lang};#{l(:label_help)}" : "en;#{l(:label_help)}") + path = File.join(File.dirname(__FILE__), + '..', '..', '..', 'assets', 'javascripts', 'lang', "dmsf_button-#{lang}.js") + lang = 'en' unless File.exist?(path) + content_for :header_tags do + javascript_include_tag("lang/dmsf_button-#{lang}", plugin: :redmine_dmsf) + + javascript_include_tag('dmsf_button', plugin: :redmine_dmsf) + + javascript_tag("jsToolBar.prototype.dmsfList = #{@dmsf_macro_list.to_json};") + end + end + end + end +end + +# Apply the patch +Redmine::WikiFormatting::Textile::Helper.prepend RedmineDmsf::Patches::FormattingHelperPatch +Redmine::WikiFormatting::CommonMark::Helper.prepend RedmineDmsf::Patches::FormattingHelperPatch diff --git a/lib/redmine_dmsf/patches/issue_patch.rb b/lib/redmine_dmsf/patches/issue_patch.rb new file mode 100644 index 00000000..86fad113 --- /dev/null +++ b/lib/redmine_dmsf/patches/issue_patch.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # Issue + module IssuePatch + ################################################################################################################## + # New methods + + def self.prepended(base) + base.class_eval do + before_destroy :delete_system_folder, prepend: true + end + end + + def save_dmsf_attachments(dmsf_attachments) + @saved_dmsf_attachments = [] + return unless dmsf_attachments + + dmsf_attachments.each_value do |dmsf_attachment| + a = Attachment.find_by_token(dmsf_attachment[:token]) + @saved_dmsf_attachments << a if a + end + end + + def saved_dmsf_attachments + @saved_dmsf_attachments || [] + end + + def save_dmsf_links(dmsf_links) + @saved_dmsf_links = [] + return unless dmsf_links + + dmsf_links.each_value do |id| + l = DmsfLink.find_by(id: id) + @saved_dmsf_links << l if l + end + end + + def saved_dmsf_links + @saved_dmsf_links || [] + end + + def save_dmsf_attachments_wfs(dmsf_attachments_wfs, dmsf_attachments) + return unless dmsf_attachments_wfs + + @dmsf_attachments_wfs = {} + dmsf_attachments_wfs.each do |attachment_id, approval_workflow_id| + attachment = dmsf_attachments[attachment_id] + next unless attachment + + a = Attachment.find_by_token(attachment[:token]) + wf = DmsfWorkflow.find_by(id: approval_workflow_id) + @dmsf_attachments_wfs[a.id] = wf if wf && a + end + end + + def saved_dmsf_attachments_wfs + @dmsf_attachments_wfs || [] + end + + def save_dmsf_links_wfs(dmsf_links_wfs) + return unless dmsf_links_wfs + + @saved_dmsf_links_wfs = {} + dmsf_links_wfs.each do |dmsf_link_id, approval_workflow_id| + wf = DmsfWorkflow.find_by(id: approval_workflow_id) + @saved_dmsf_links_wfs[dmsf_link_id.to_i] = wf if wf + end + end + + def saved_dmsf_links_wfs + @saved_dmsf_links_wfs || {} + end + + def main_system_folder(create: false, prj_id: nil) + prj_id ||= project_id + parent = DmsfFolder.issystem.find_by(project_id: prj_id, title: '.Issues') + if create && !parent + parent = DmsfFolder.new + parent.project_id = prj_id + parent.title = '.Issues' + parent.description = 'Documents assigned to issues' + parent.user_id = User.anonymous.id + parent.system = true + parent.save + end + parent + end + + def system_folder(create: false, prj_id: nil) + prj_id ||= project_id + parent = main_system_folder(create: create, prj_id: prj_id) + if parent + folder = DmsfFolder.issystem + .where(project_id: prj_id, dmsf_folder_id: parent.id) + .where(['title LIKE :con', { con: "#{id} - %" }]) + .first + if create && !folder + folder = DmsfFolder.new + folder.dmsf_folder_id = parent.id + folder.project_id = prj_id + folder.title = "#{id} - #{DmsfFolder.get_valid_title(subject)}" + folder.user_id = User.anonymous.id + folder.system = true + folder.save + end + end + folder + end + + def dmsf_files + system_folder&.dmsf_files&.visible || [] + end + + def dmsf_links + system_folder&.dmsf_links&.visible || [] + end + + def delete_system_folder + system_folder&.destroy + end + + def dmsf_file_added(dmsf_file) + journalize_dmsf_file dmsf_file, :added + end + + def dmsf_file_removed(dmsf_file) + journalize_dmsf_file dmsf_file, :removed + end + + # Adds a journal detail for an attachment that was added or removed + def journalize_dmsf_file(dmsf_file, added_or_removed) + key = (added_or_removed == :removed ? :old_value : :value) + init_journal User.current + current_journal.details << JournalDetail.new( + property: 'dmsf_file', + prop_key: dmsf_file.id, + key => dmsf_file.title + ) + current_journal.save + end + end + end +end + +# Apply patch +Issue.prepend RedmineDmsf::Patches::IssuePatch diff --git a/lib/redmine_dmsf/patches/notifiable_patch.rb b/lib/redmine_dmsf/patches/notifiable_patch.rb new file mode 100644 index 00000000..b8e4bf4d --- /dev/null +++ b/lib/redmine_dmsf/patches/notifiable_patch.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # Notifiable + module NotifiablePatch + def self.prepended(base) + base.singleton_class.prepend(ClassMethods) + end + + # Class methods + module ClassMethods + ################################################################################################################ + # Overridden methods + # + def all + notifications = super + notifications << Redmine::Notifiable.new('dmsf_workflow_plural') + notifications << Redmine::Notifiable.new('dmsf_legacy_notifications') + notifications + end + end + end + end +end + +# Apply the patch +Redmine::Notifiable.prepend RedmineDmsf::Patches::NotifiablePatch unless RedmineDmsf::Plugin.an_obsolete_plugin_present? diff --git a/lib/redmine_dmsf/patches/notifiable_ru_patch.rb b/lib/redmine_dmsf/patches/notifiable_ru_patch.rb new file mode 100644 index 00000000..63f67677 --- /dev/null +++ b/lib/redmine_dmsf/patches/notifiable_ru_patch.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # TODO: This is just a workaround to fix alias_method usage in RedmineUp's plugins, which is in conflict with + # prepend and causes an infinite loop. + module NotifiableRuPatch + def self.included(base) + base.extend ClassMethods + base.class_eval do + class << self + alias_method :all_without_resources_dmsf, :all + alias_method :all, :all_with_resources_dmsf + end + end + end + + # Class methods + module ClassMethods + def all_with_resources_dmsf + notifications = all_without_resources_dmsf + notifications << Redmine::Notifiable.new('dmsf_workflow_plural') + notifications << Redmine::Notifiable.new('dmsf_legacy_notifications') + notifications + end + end + end + end +end + +# Apply the patch +Redmine::Notifiable.include RedmineDmsf::Patches::NotifiableRuPatch if RedmineDmsf::Plugin.an_obsolete_plugin_present? diff --git a/lib/redmine_dmsf/patches/pdf_patch.rb b/lib/redmine_dmsf/patches/pdf_patch.rb new file mode 100644 index 00000000..93e8084a --- /dev/null +++ b/lib/redmine_dmsf/patches/pdf_patch.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Redmine's PDF export patch to view DMS images + +require 'redmine/export/pdf' + +module RedmineDmsf + module Patches + # PDF + module PdfPatch + ################################################################################################################## + # Overridden methods + + 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 + else + super + end + end + end + end +end + +# Apply the patch +Redmine::Export::PDF::ITCPDF.prepend RedmineDmsf::Patches::PdfPatch diff --git a/lib/redmine_dmsf/patches/project_patch.rb b/lib/redmine_dmsf/patches/project_patch.rb new file mode 100644 index 00000000..62a4d826 --- /dev/null +++ b/lib/redmine_dmsf/patches/project_patch.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# This program is free software; you can redistribute it and/or +# 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 +# . + +module RedmineDmsf + module Patches + # Project + module ProjectPatch + ################################################################################################################## + # Overridden methods + + def initialize(attributes = nil, *args) + super + self.watcher_user_ids = [] if new_record? + end + + def copy(project, options = {}) + super + project = Project.find(project) unless project.is_a?(Project) + to_be_copied = %w[dmsf dmsf_folders approval_workflows] + to_be_copied &= Array.wrap(options[:only]) if options[:only] + if save + to_be_copied.each do |name| + send :"copy_#{name}", project + end + save + else + false + end + end + + ################################################################################################################## + # New methods + def self.prepended(base) + base.class_eval do + has_many :dmsf_files, -> { where(dmsf_folder_id: nil).order(:name) }, + class_name: 'DmsfFile', foreign_key: 'project_id', dependent: :destroy + has_many :dmsf_folders, -> { where(dmsf_folder_id: nil).order(:title) }, + 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' }, + class_name: 'DmsfLink', foreign_key: 'project_id', dependent: :destroy + has_many :file_links, -> { where dmsf_folder_id: nil, target_type: 'DmsfFile' }, + class_name: 'DmsfLink', foreign_key: 'project_id', dependent: :destroy + has_many :url_links, -> { where dmsf_folder_id: nil, target_type: 'DmsfUrl' }, + class_name: 'DmsfLink', foreign_key: 'project_id', dependent: :destroy + has_many :dmsf_links, -> { where dmsf_folder_id: nil }, + class_name: 'DmsfLink', foreign_key: 'project_id', dependent: :destroy + + belongs_to :default_dmsf_query, class_name: 'DmsfQuery' + + acts_as_watchable + + before_save :set_default_dmsf_notification + + validates_length_of :dmsf_description, maximum: 65_535 + + const_set :ATTACHABLE_DMS_AND_ATTACHMENTS, 1 + const_set :ATTACHABLE_ATTACHMENTS, 2 + end + end + + def set_default_dmsf_notification + return unless new_record? + return unless !dmsf_notification && RedmineDmsf.dmsf_default_notifications? + + self.dmsf_notification = true + end + + def dmsf_count + file_count = DmsfFile.visible.where(project_id: id).all.size + folder_count = DmsfFolder.visible.where(project_id: id).all.size + { files: file_count, folders: folder_count } + end + + # Simple yet effective approach to copying things + def copy_dmsf(project) + copy_dmsf_folders project, copy_files: true + project.dmsf_files.visible.each do |f| + f.copy_to self, nil + end + project.file_links.visible.each do |l| + l.copy_to self, nil + end + project.url_links.visible.each do |l| + l.copy_to self, nil + end + end + + def copy_dmsf_folders(project, copy_files: false) + project.dmsf_folders.visible.each do |f| + f.copy_to self, nil, copy_files: copy_files + end + project.folder_links.visible.each do |l| + l.copy_to self, nil + end + end + + def copy_approval_workflows(project) + project.dmsf_workflows.each do |wf| + wf.copy_to self + end + end + + # Go recursively through the project tree until a dmsf enabled project is found + def dmsf_available? + return true if visible? && module_enabled?(:dmsf) && User.current&.allowed_to?(:view_dmsf_folders, self) + + children.each do |child| + return true if child.dmsf_available? + end + false + end + end + end +end + +# Apply the patch +Project.prepend RedmineDmsf::Patches::ProjectPatch diff --git a/lib/redmine_dmsf/patches/projects_helper_patch.rb b/lib/redmine_dmsf/patches/projects_helper_patch.rb new file mode 100644 index 00000000..7e999404 --- /dev/null +++ b/lib/redmine_dmsf/patches/projects_helper_patch.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # Project helper + module ProjectsHelperPatch + ################################################################################################################## + # Overridden methods + + def project_settings_tabs + tabs = super + dmsf_tabs = + [ + { + name: 'dmsf', + action: { controller: 'dmsf_state', action: 'user_pref_save' }, + partial: 'dmsf_state/user_pref', label: :menu_dmsf + }, + { + name: 'dmsf_workflow', + action: { controller: 'dmsf_workflows', action: 'index' }, + partial: 'dmsf_workflows/main', label: :label_dmsf_workflow_plural + } + ] + tabs.concat( + dmsf_tabs.select { |dmsf_tab| User.current.allowed_to?(dmsf_tab[:action], @project) } + ) + tabs + end + end + end +end + +# Apply the patch +ProjectsController.send(:helper, RedmineDmsf::Patches::ProjectsHelperPatch) diff --git a/lib/redmine_dmsf/patches/puma_patch.rb b/lib/redmine_dmsf/patches/puma_patch.rb new file mode 100644 index 00000000..46966d56 --- /dev/null +++ b/lib/redmine_dmsf/patches/puma_patch.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +# Redmine's PDF export patch to view DMS images + +module RedmineDmsf + module Patches + # Puma + module PumaPatch + ################################################################################################################## + # Overridden methods + def self.included(base) + base.class_eval do + # WebDAV methods + methods = Puma::Const::SUPPORTED_HTTP_METHODS | + %w[OPTIONS HEAD GET PUT POST DELETE PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK] + remove_const :SUPPORTED_HTTP_METHODS + const_set :SUPPORTED_HTTP_METHODS, methods.freeze + end + end + end + end +end + +# Apply the patch +Puma::Const.include RedmineDmsf::Patches::PumaPatch if RedmineDmsf::Plugin.lib_available?('puma/const') diff --git a/lib/redmine_dmsf/patches/queries_controller_patch.rb b/lib/redmine_dmsf/patches/queries_controller_patch.rb new file mode 100644 index 00000000..3b2a3304 --- /dev/null +++ b/lib/redmine_dmsf/patches/queries_controller_patch.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # Queries controller + module QueriesControllerPatch + ################################################################################################################## + # New methods + + private + + def redirect_to_dmsf_query(options) + if @project + redirect_to dmsf_folder_path(@project, options) + else + redirect_to home_path(options) + end + end + end + end +end + +# Apply the patch +QueriesController.prepend RedmineDmsf::Patches::QueriesControllerPatch diff --git a/lib/redmine_dmsf/patches/role_patch.rb b/lib/redmine_dmsf/patches/role_patch.rb new file mode 100644 index 00000000..e9d84492 --- /dev/null +++ b/lib/redmine_dmsf/patches/role_patch.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # Role + module RolePatch + ################################################################################################################## + # New methods + + def self.included(base) + base.class_eval do + before_destroy :remove_dmsf_references, prepend: true + end + end + + def remove_dmsf_references + return unless id + + substitute = Role.anonymous + DmsfFolderPermission.where(object_id: id, object_type: 'Role').update_all object_id: substitute.id + end + end + end +end + +# Apply the patch +Role.prepend RedmineDmsf::Patches::RolePatch diff --git a/lib/redmine_dmsf/patches/search_patch.rb b/lib/redmine_dmsf/patches/search_patch.rb new file mode 100644 index 00000000..c3eb24c0 --- /dev/null +++ b/lib/redmine_dmsf/patches/search_patch.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # Search patch + module SearchPatch + ################################################################################################################## + # Overridden methods + def self.prepended(base) + base.singleton_class.prepend(ClassMethods) + end + + # Class methods + module ClassMethods + def available_search_types + # Removes the original Documents from searching (replaced with DMSF) + if RedmineDmsf.remove_original_documents_module? + super.reject { |t| t == 'documents' } + else + super + end + end + end + end + end +end + +# Apply the patch +Redmine::Search.prepend RedmineDmsf::Patches::SearchPatch diff --git a/lib/redmine_dmsf/patches/user_patch.rb b/lib/redmine_dmsf/patches/user_patch.rb new file mode 100644 index 00000000..e6a997c8 --- /dev/null +++ b/lib/redmine_dmsf/patches/user_patch.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # User + module UserPatch + ################################################################################################################## + # New methods + + def self.prepended(base) + base.class_eval do + before_destroy :remove_dmsf_references, prepend: true + end + end + + private + + def remove_dmsf_references + return if id.nil? + + substitute = User.anonymous + DmsfFileRevisionAccess.where(user_id: id).update_all user_id: substitute.id + DmsfFileRevision.where(user_id: id).update_all user_id: substitute.id + DmsfFileRevision.where(dmsf_workflow_assigned_by_user_id: id).update_all( + dmsf_workflow_assigned_by_user_id: substitute.id + ) + DmsfFileRevision.where(dmsf_workflow_started_by_user_id: id).update_all( + dmsf_workflow_started_by_user_id: substitute.id + ) + DmsfFileRevision.where(user_id: id).update_all user_id: substitute.id + DmsfFile.where(deleted_by_user_id: id).update_all deleted_by_user_id: substitute.id + DmsfFolder.where(user_id: id).update_all user_id: substitute.id + DmsfFolder.where(deleted_by_user_id: id).update_all deleted_by_user_id: substitute.id + DmsfLink.where(user_id: id).update_all user_id: substitute.id + DmsfLink.where(deleted_by_user_id: id).update_all deleted_by_user_id: substitute.id + DmsfLock.where(user_id: id).delete_all + DmsfWorkflowStepAction.where(author_id: id).update_all author_id: substitute.id + DmsfWorkflowStepAssignment.where(user_id: id).update_all user_id: substitute.id + DmsfWorkflowStep.where(user_id: id).update_all user_id: substitute.id + DmsfWorkflow.where(author_id: id).update_all author_id: substitute.id + DmsfFolderPermission.where(object_id: id, object_type: 'User').update_all object_id: substitute.id + end + end + end +end + +# Apply the patch +User.prepend RedmineDmsf::Patches::UserPatch diff --git a/lib/redmine_dmsf/patches/user_preference_patch.rb b/lib/redmine_dmsf/patches/user_preference_patch.rb new file mode 100644 index 00000000..67332a13 --- /dev/null +++ b/lib/redmine_dmsf/patches/user_preference_patch.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Patches + # User preference + module UserPreferencePatch + ################################################################################################################## + # New methods + + UserPreference.safe_attributes 'dmsf_attachments_upload_choice' + + def dmsf_attachments_upload_choice + self[:dmsf_attachments_upload_choice] || 'DMSF' + end + + def dmsf_attachments_upload_choice=(value) + self[:dmsf_attachments_upload_choice] = value + end + + UserPreference.safe_attributes 'default_dmsf_query' + + def default_dmsf_query + self[:default_dmsf_query] || nil + end + + def default_dmsf_query=(value) + self[:default_dmsf_query] = value + end + + UserPreference.safe_attributes 'receive_download_notification' + + def receive_download_notification + self[:receive_download_notification] || '0' + end + + def receive_download_notification=(value) + self[:receive_download_notification] = value + end + end + end +end + +# Apply the patch +UserPreference.prepend RedmineDmsf::Patches::UserPreferencePatch diff --git a/lib/redmine_dmsf/plugin.rb b/lib/redmine_dmsf/plugin.rb new file mode 100644 index 00000000..ffe5f2d9 --- /dev/null +++ b/lib/redmine_dmsf/plugin.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + # Plugin + module Plugin + # Checking physical presence of the plugin as Redmine::Plugin.installed? may return false due to alphabetical + # registering of available plugins. + def self.present?(id) + Rails.root.join('plugins', id.to_s).exist? + end + + # Return true if a plugin that overrides Redmine::Notifiable and use the deprecated method alias_method_chain is + # present. + # It is related especially to plugins made by AlphaNode and RedmineUP. + def self.an_obsolete_plugin_present? + plugins = %w[redmine_questions redmine_db redmine_passwords redmine_resources redmine_products redmine_finance] + plugins.each do |plugin| + return true if Plugin.present?(plugin) + 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 diff --git a/lib/redmine_dmsf/preview.rb b/lib/redmine_dmsf/preview.rb new file mode 100644 index 00000000..130762b6 --- /dev/null +++ b/lib/redmine_dmsf/preview.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +require 'English' + +module RedmineDmsf + # Preview + module Preview + extend Redmine::Utils::Shell + include Redmine::I18n + + def self.office_available? + return @office_available if defined?(@office_available) + + begin + office_bin = RedmineDmsf.office_bin.presence || 'libreoffice' + `#{shell_quote office_bin} --version` + @office_available = $CHILD_STATUS.success? + rescue StandardError + @office_available = false + end + Rails.logger.warn l(:note_dmsf_office_bin_not_available, value: office_bin, locale: :en) unless @office_available + @office_available + end + + def self.generate(source, target) + return target if File.exist?(target) + + dir = File.dirname(target) + 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) + target + else + Rails.logger.error "Creating preview failed (#{$CHILD_STATUS}):\nCommand: #{cmd}" + '' + end + end + end +end diff --git a/lib/redmine_dmsf/webdav/base_resource.rb b/lib/redmine_dmsf/webdav/base_resource.rb new file mode 100644 index 00000000..e32db4c9 --- /dev/null +++ b/lib/redmine_dmsf/webdav/base_resource.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require 'addressable/uri' + +module RedmineDmsf + module Webdav + # Base resource + class BaseResource < Dav4rack::Resource + include Redmine::I18n + include ActionView::Helpers::NumberHelper + + attr_reader :public_path + + DIR_FILE = %( + +
          %s + %s + %s + %s + + ) + + def initialize(path, request, response, options) + raise NotFound unless RedmineDmsf.dmsf_webdav? + + @project = nil + @public_path = "#{options[:root_uri_path]}#{path}" + @children = nil + super + end + + def accessor=(klass) + @__proxy = klass + end + + # Overridable function to provide better listing for GET requests + def long_name + nil + end + + # Overridable function to provide better listing for GET requests + def special_type + nil + end + + # Overridden + def index_page + %( + + + + %s + + + +

          %s

          +
          + + + + + + + + %s +
          #{l(:field_name)}#{l(:field_filesize)}#{l(:field_type)}#{l(:link_modified)}
          +
          + + + ) + end + + # Generate HTML for Get requests, or Head requests if no_body is true + def html_display + @response.body = +'' + return Conflict unless collection? + + entities = children.map do |child| + format(DIR_FILE, + uri_encode(request.url_for(child.path)), + child.long_name || child.name, + child.collection? ? '' : number_to_human_size(child.content_length), + child.special_type || child.content_type, + child.last_modified) + end + entities *= "\n" + if parent + entities = format(DIR_FILE, + uri_encode(request.url_for(parent.path)), + l(:parent_directory), + '', + '', + '') + entities + end + @response.body << format(index_page, @path.empty? ? '/' : @path, @path.empty? ? '/' : @path, entities) + end + + # Run method through proxy class - ensuring always compatible child is generated + def child(name) + new_path = normalized_path + ResourceProxy.new "#{new_path}#{name}", request, response, @options.merge(user: @user) + end + + def child_project(project) + project_display_name = ProjectResource.create_project_name(project) + new_path = normalized_path + new_path += project_display_name + ResourceProxy.new new_path, request, response, @options.merge(user: @user, project: true) + end + + def parent + p = @__proxy.parent + return nil unless p + + p.resource.nil? ? p : p.resource + end + + def options(_request, response) + response['Allow'] ||= 'OPTIONS,HEAD,GET,PROPFIND' if @__proxy.read_only + OK + end + + def project + resource_info + @project + end + + def subproject + resource_info + @subproject + end + + def folder + resource_info + @folder + end + + def file + resource_info + @file + end + + class << self + def get_project(scope, name, parent_project) + prj = nil + scope = scope.where(parent_id: parent_project.id) if parent_project + if RedmineDmsf.dmsf_webdav_use_project_names? + if name =~ /^\[?.+ (\d+)\]?$/ + prj = scope.find_by(id: Regexp.last_match(1)) + # Check again whether it's really the project and not a folder with a number as a suffix + prj = nil if prj && !name.start_with?("[#{DmsfFolder.get_valid_title(prj.name)}") + end + else + identifier = if name.start_with?('[') && name.end_with?(']') + name[1..-2] + else + name + end + prj = scope.find_by(identifier: identifier) + end + prj + end + end + + protected + + # Add slash at the beginning and the end if missing + def normalized_path + new_path = @path + new_path << '/' unless new_path.end_with?('/') + new_path.start_with?('/') ? new_path : new_path.insert(0, '/') + end + + def uri_encode(uri) + uri.gsub(/[()&\[\]]/, '(' => '%28', ')' => '%29', '&' => '%26', '[' => '%5B', ']' => '5D') + end + + def basename + File.basename @path + end + + def path_prefix + @public_path.gsub(/#{Regexp.escape(path)}$/, '') + end + + def load_projects(project_scope) + scope = project_scope.visible + scope = scope.non_templates if scope.respond_to?(:non_templates) + scope.find_each do |p| + @children << child_project(p) if p.dmsf_available? + end + end + + # Adds the given xml namespace to namespaces and returns the prefix + def add_namespace(namespace, prefix = "unknown#{rand 65_536}") + @__proxy.add_namespace namespace, prefix + end + + # returns the prefix for the given namespace, adding it if necessary + def prefix_for(ns_href) + @__proxy.prefix_for ns_href + end + + private + + def resource_info + return if @project # We have already got it + + pinfo = @path.split('/').drop(1) + i = 1 + project_scope = Project.visible + project_scope = project_scope.non_templates if project_scope.respond_to?(:non_templates) + until pinfo.empty? + prj = BaseResource.get_project(project_scope, pinfo.first, @project) + if prj + @project = prj + if pinfo.length == 1 + @subproject = @project + break # We're at the end + end + else + @subproject = nil + fld = get_folder(pinfo.first) + if fld + @folder = fld + else + @file = DmsfFile.find_file_by_name(@project, @folder, pinfo.first) + @folder = nil + unless pinfo.length < 2 || @file + Rails.logger.error "Resource not found: #{@path}" + raise Conflict + end + break # We're at the end + end + end + i += 1 + pinfo = path.split('/').drop(i) + end + end + + def get_folder(name) + return nil unless @project + + f = DmsfFolder.visible.find_by(project_id: @project.id, dmsf_folder_id: @folder&.id, title: name) + f && !DmsfFolder.permissions?(f, allow_system: false) ? nil : f + end + end + end +end diff --git a/lib/redmine_dmsf/webdav/custom_middleware.rb b/lib/redmine_dmsf/webdav/custom_middleware.rb new file mode 100644 index 00000000..a1fce95b --- /dev/null +++ b/lib/redmine_dmsf/webdav/custom_middleware.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require "#{File.dirname(__FILE__)}/../../dav4rack" +require "#{File.dirname(__FILE__)}/resource_proxy" +require "#{File.dirname(__FILE__)}/dmsf_controller" + +module RedmineDmsf + module Webdav + AUTHENTICATION_REALM = 'DMSF content' + # Custom middleware + class CustomMiddleware + def initialize(app) + @rails_app = app + path = '/dmsf/webdav' + @dav_app = Rack::Builder.new do + map path do + run Dav4rack::Handler.new( + root_uri_path: path, + resource_class: RedmineDmsf::Webdav::ResourceProxy, + allow_unauthenticated_options_on_root: true, + controller_class: RedmineDmsf::Webdav::DmsfController + ) + end + end + end + + def call(env) + begin + status, headers, body = @dav_app.call env + rescue StandardError => e + Rails.logger.error e.message + status = defined?(e.to_i) ? e : 500 + headers = {} + body = [''] + end + # If the URL map generated by Rack::Builder did not find a matching path, + # it will return a 404 along with the x-cascade header set to 'pass'. + if (status == 404) && (headers['x-cascade'] == 'pass') + @rails_app.call env # let Rails handle the request + else + [status, headers, body] + end + end + end + end +end diff --git a/lib/redmine_dmsf/webdav/dmsf_controller.rb b/lib/redmine_dmsf/webdav/dmsf_controller.rb new file mode 100644 index 00000000..93b6074d --- /dev/null +++ b/lib/redmine_dmsf/webdav/dmsf_controller.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require "#{File.dirname(__FILE__)}/dmsf_digest" + +module RedmineDmsf + module Webdav + # DMSF controller + class DmsfController < Dav4rack::Controller + include AbstractController::Callbacks + + around_action :switch_locale + + def switch_locale(&action) + # Switch the locale to English for WebDAV requests in order to have log messages in English + I18n.with_locale(:en, &action) + end + + def process + return super unless RedmineDmsf.dmsf_webdav_authentication == 'Digest' + + status = skip_authorization? || authenticate? ? process_action || OK : Dav4rack::HttpStatus::Unauthorized + rescue Dav4rack::HttpStatus::Status => e + status = e + ensure + if status + response.status = status.code + if status.code == 401 + time_stamp = Time.now.to_i + h_once = ActiveSupport::Digest.hexdigest("#{time_stamp}:#{SecureRandom.hex(32)}") + nonce = Base64.strict_encode64("#{time_stamp}#{h_once}") + response['WWW-Authenticate'] = + %(Digest realm="#{authentication_realm}", nonce="#{nonce}", algorithm="MD5", qop="auth") + end + end + end + + def authenticate? + return super unless RedmineDmsf.dmsf_webdav_authentication == 'Digest' + + auth_header = request.authorization.to_s + scheme = auth_header.split(' ', 2).first&.downcase + if scheme == 'digest' + Rails.logger.info 'Authentication: digest' + digest = DmsfDigest.new(request.authorization) + params = digest.params + username = params['username'] + response = params['response'] + cnonce = params['cnonce'] + nonce = params['nonce'] + uri = params['uri'] + qop = params['qop'] + nc = params['nc'] + user = User.find_by(login: username) + unless user + Rails.logger.error "Digest authentication: #{username} not found" + raise Unauthorized + end + unless user.active? + Rails.logger.error l(:notice_account_locked) + raise Unauthorized + end + token = Token.find_by(user_id: user.id, action: 'dmsf_webdav_digest') + unless token + Rails.logger.error "Digest authentication: no digest found for #{username}" + raise Unauthorized + end + ha1 = token.value + ha2 = ActiveSupport::Digest.hexdigest("#{request.env['REQUEST_METHOD']}:#{uri}") + required_response = if qop + ActiveSupport::Digest.hexdigest("#{ha1}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{ha2}") + else + ActiveSupport::Digest.hexdigest("#{ha1}:#{nonce}:#{ha2}") + end + if required_response == response + User.current = user + else + Rails.logger.error 'Digest authentication: digest response is incorrect' + end + else + Rails.logger.warn "Digest authentication method expected got '#{scheme}'" + end + raise Unauthorized if User.current.anonymous? + + Rails.logger.info "Current user: #{User.current}, User-Agent: #{request.user_agent}" + User.current && !User.current.anonymous? + end + end + end +end diff --git a/lib/redmine_dmsf/webdav/dmsf_digest.rb b/lib/redmine_dmsf/webdav/dmsf_digest.rb new file mode 100644 index 00000000..110e2cce --- /dev/null +++ b/lib/redmine_dmsf/webdav/dmsf_digest.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Webdav + # Replacement for Rack::Auth::Digest + class DmsfDigest + def initialize(authorization) + @authorization = authorization + end + + def params + params = {} + parts = @authorization.split(' ', 2) + split_header_value(parts[1]).each do |param| + k, v = param.split('=', 2) + params[k] = dequote(v) + end + params + end + + private + + def dequote(str) + ret = /\A"(.*)"\Z/ =~ str ? ::Regexp.last_match(1) : str.dup + ret.gsub(/\\(.)/, '\\1') + end + + def split_header_value(str) + str.scan(/(\w+=(?:"[^"]+"|[^,]+))/n).pluck(0) + end + end + end +end diff --git a/lib/redmine_dmsf/webdav/dmsf_resource.rb b/lib/redmine_dmsf/webdav/dmsf_resource.rb new file mode 100644 index 00000000..406cc568 --- /dev/null +++ b/lib/redmine_dmsf/webdav/dmsf_resource.rb @@ -0,0 +1,806 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require 'uuidtools' +require 'addressable/uri' + +module RedmineDmsf + module Webdav + # DMSF resource + class DmsfResource < BaseResource + include Redmine::I18n + + # name:: String - Property name + # Returns the value of the given property + def get_property(element) + if element[:ns_href] == DAV_NAMESPACE + super + else + custom_property element + end + end + + # name:: String - Property name + # value:: New value + # Set the property to the given value + def set_property(element, value) + # let Resource handle DAV properties + if element[:ns_href] == DAV_NAMESPACE + super + else + set_custom_property element, value + end + end + + # name:: Property name + # Remove the property from the resource + def remove_property(element) + Redmine::Search.cache_store.delete "#{property_key}-#{element[:name]}" + end + + # Gather collection of objects that denote current entities child entities + # 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 + unless @children + @children = [] + if folder + # Folders + folder.dmsf_folders.visible.each do |f| + @children.push child(f.title) if DmsfFolder.permissions?(f, allow_system: false) + end + # Files + folder.dmsf_files.visible.pluck(:name).each do |name| + @children.push child(name) + end + end + end + @children + end + + # Does the object exist? + # If it is either a subproject or a folder or a file, then it exists + def exist? + project&.module_enabled?('dmsf') && (subproject || folder || file) && + (User.current.admin? || User.current.allowed_to?(:view_dmsf_folders, project)) + end + + # Is this entity a folder? + def collection? + folder || subproject + end + + # Return the content type of file + # will return inode/directory for any collections, and appropriate for File entities + def content_type + if file + if file.last_revision + file.last_revision.detect_content_type + else + 'application/octet-stream' + end + else + 'inode/directory' + end + end + + def creation_date + if folder + folder.created_at + elsif file + file.created_at + else + raise NotFound + end + end + + def last_modified + if folder + folder.updated_at + elsif file + if file.last_revision + file.last_revision.updated_at + else + file.updated_at + end + else + raise NotFound + end + 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 '%x-%x-%x', + node: ino, size: content_length, modified: (last_modified ? last_modified.to_i : 0) + end + + def content_length + file ? file.size : 4096 + end + + def special_type + l(:field_folder) if folder + end + + # Process incoming GET request + # If instance is a collection, calls html_display (defined in base_resource.rb) which cycles through children for + # display. File will only be presented for download if user has permission to view files. + def get(request, response) + raise Forbidden unless !parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder) + + if collection? + html_display + response['Content-Length'] = response.body.bytesize.to_s + response['Content-Type'] = 'text/html' + else + raise Forbidden unless User.current.admin? || User.current.allowed_to?(:view_dmsf_files, project) + + http_if_none_match = request.get_header('HTTP_IF_NONE_MATCH') + # MS Office 2016, PROTECTED VIEW => Enable editing? + return NotModified if http_if_none_match.present? && (http_if_none_match == etag) + + response.body = download # Rack based provider + end + OK + end + + # Process incoming MKCOL request + # Create a DmsfFolder at location requested, only if parent is a folder (or root) + # Ensure item is only functional if project is enabled for dmsf + def make_collection + if request.body.read.to_s.empty? + raise NotFound unless project&.module_enabled?('dmsf') + raise Forbidden unless User.current.admin? || User.current.allowed_to?(:folder_manipulation, project) + unless !parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, allow_system: false) + raise Forbidden + end + + f = DmsfFolder.new + f.title = basename + f.dmsf_folder_id = parent.folder&.id + f.project = project + f.user = User.current + f.save ? Created : Conflict + else + UnsupportedMediaType + end + end + + # Process incoming DELETE request + # should be of entity to be deleted, we simply follow the Dmsf entity method + # for deletion and return of appropriate status based on outcome. + def delete + if file + raise Forbidden unless User.current.admin? || User.current.allowed_to?(:file_delete, project) + unless !parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, allow_system: false) + raise Forbidden + end + raise Locked if file.locked_for_user? + + pattern = RedmineDmsf.dmsf_webdav_disable_versioning + # Files that are not versioned should be destroyed + # Zero-sized files should be destroyed + b = !file.last_revision || file.last_revision.size.zero? + destroy = basename.match(pattern) || b + if file.delete(commit: destroy) + DmsfMailer.deliver_files_deleted project, [file] + NoContent + else + Conflict + end + elsif folder + raise Locked if folder.locked? + + # To fulfill Litmus requirements to not delete folder if fragments are in the URL + uri = URI(uri_encode(request.get_header('REQUEST_URI'))) + raise BadRequest if uri.fragment.present? + raise Forbidden unless User.current.admin? || User.current.allowed_to?(:folder_manipulation, project) + raise Forbidden unless DmsfFolder.permissions?(folder, allow_system: false) + + folder.delete(commit: false) ? NoContent : Conflict + else + MethodNotAllowed + end + end + + # Process incoming MOVE request + # Behavioural differences between collection and single entity + def move(dest_path) + 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 + 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 + end + return PreconditionFailed if dest.exist? && request.get_header('HTTP_OVERWRITE') == 'F' + + if collection? + if dest.exist? + d = dest.collection? + d&.delete(commit: true) if folder && request.get_header('HTTP_OVERWRITE') == 'T' + end + if !User.current.admin? && (!User.current.allowed_to?(:folder_manipulation, project) || + !User.current.allowed_to?(:folder_manipulation, dest.resource.project)) + raise Forbidden + end + return MethodNotAllowed unless folder # Moving sub-project 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 + if !User.current.admin? && (!User.current.allowed_to?(:file_manipulation, project) || + !User.current.allowed_to?(:file_manipulation, dest.resource.project)) + raise Forbidden + end + raise Locked if file.locked_for_user? + + if dest.exist? && !dest.collection? + if dest.resource.file.last_revision.size.zero? || reuse_version_for_locked_file?(dest.resource.file) + # Last revision in the destination has zero size so reuse that revision + new_revision = dest.resource.file.last_revision + else + # Create a new revision by cloning the last revision in the destination + 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 + 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 + # 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 + # in case of a copy + request.get_header('HTTP_OVERWRITE') == 'T' && file.delete(commit: true) ? Created : PreconditionFailed + else + return PreconditionFailed unless exist? && file + + if (project == dest.resource.project) && dest.resource.basename.match(/.\.tmp$/i) + Rails.logger.info do + "WebDAV MOVE: #{file.name} -> #{dest.resource.basename}, possible MSOffice rename to .tmp when saving." + end + # Renaming the file to X.tmp, might be Office that is saving a file. Keep the original file. + file.copy_to_filename dest.resource.project, parent&.folder, dest.resource.basename + Created + else + if (project == dest.resource.project) && file.last_revision.size.zero? + # Moving a zero sized file within the same project, just update the dmsf_folder + file.dmsf_folder = parent&.folder + else + return InternalServerError unless file.move_to(dest.resource.project, parent&.folder) + end + # 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) + end + file.name = dest.resource.basename + # Save Changes + if file.last_revision.save && file.save + dest.exist? ? NoContent : Created + else + PreconditionFailed + end + end + end + end + end + + # Process incoming COPY request + # Behavioural differences between collection and single entity + def copy(dest) + dest = ResourceProxy.new(dest, @request, @response, @options.merge(user: @user)) + return PreconditionFailed unless dest.resource.project + + parent = dest.resource.parent + unless !parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, allow_system: false) + raise Forbidden + end + + return Conflict unless dest.parent.exist? + + res = Created + if dest.exist? + return Locked if dest.lockdiscovery.present? + return PreconditionFailed if request.get_header('HTTP_OVERWRITE') == 'F' + + dest.delete if request.get_header('HTTP_OVERWRITE') == 'T' + + res = NoContent + end + return PreconditionFailed unless parent.exist? && parent.folder + + if collection? + # Permission check if they can manipulate folders and view folders + # Can they: + # 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 + 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) && + User.current.allowed_to?(:view_dmsf_files, project) && + User.current.allowed_to?(:view_dmsf_folders, project)) + raise Forbidden unless DmsfFolder.permissions?(folder, allow_system: false) + + folder.title = dest.resource.basename + new_folder = folder.copy_to(dest.resource.project, parent.folder) + return PreconditionFailed if new_folder.nil? || new_folder.id.nil? + + Created + else + # Permission check if they can manipulate folders and view folders + # Can they: + # Manipulate files on destination project :file_manipulation + # View files on destination project :view_dmsf_files + # View files on the source project :view_dmsf_files + raise Forbidden unless User.current.admin? || + (User.current.allowed_to?(:file_manipulation, dest.resource.project) && + User.current.allowed_to?(:view_dmsf_files, dest.resource.project) && + User.current.allowed_to?(:view_dmsf_files, project)) + return PreconditionFailed unless exist? && file + + new_file = file.copy_to(dest.resource.project, parent&.folder) + return InternalServerError unless new_file&.last_revision + + # 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 + # Save Changes + new_file.last_revision.save && new_file.save ? res : PreconditionFailed + end + end + + # Lock Check + # Check for the existence of locks + def lock_check(args = {}) + entity = file || folder + return unless entity + + refresh = args && !args[:scope] && !args[:type] + args ||= {} + args[:method] = @request.request_method.downcase + http_if = request.get_header('HTTP_IF') + if http_if.present? + no_lock = http_if.include?('') + not_no_lock = http_if.include?('Not ') + # Invalid lock token + if http_if =~ /\(<([a-f0-9]+-[a-f0-9]+-[a-f0-9]+-[a-f0-9]+-[a-f0-9]+)>/ + raise PreconditionFailed unless entity.locked? + raise Locked if Regexp.last_match(1) != entity.lock.first.uuid && entity.locked_for_user?(args) + elsif (!no_lock || not_no_lock) && entity.locked_for_user?(args) + raise Locked + else + raise PreconditionFailed + end + # Invalid etag + if http_if =~ /^\(<([a-f0-9]+-[a-f0-9]+-[a-f0-9]+-[a-f0-9]+-[a-f0-9]+)> \[([a-f0-9]+-[a-f0-9]+-[a-f0-9]+)\]/ + return if Regexp.last_match(2) == etag # lock & etag + + raise PreconditionFailed + end + # no-lock + return if no_lock + + end + return unless entity.locked_for_user?(args) && !refresh + + if http_if.present? + case args[:method] + when 'put', 'proppatch' + return + end + end + raise Locked + end + + # Lock + def lock(args) + unless parent&.exist? + e = Dav4rack::LockFailure.new + e.add_failure @path, Conflict + raise e + end + unless exist? + # A successful lock request to an unmapped URL MUST result in the creation of a locked (non-collection) + # resource with empty content. + return NoContent if ignore? + + f = create_empty_file + if f + scope = :"scope_#{args[:scope] || 'exclusive'}" + type = :"type_#{args[:type] || 'write'}" + l = f.lock!(scope, type, 1.week.from_now, args[:owner]) + @response['Lock-Token'] = l.uuid + return [1.week.to_i, l.uuid] + else + e = Dav4rack::LockFailure.new + e.add_failure @path, NotFound + raise e + end + end + lock_check args + entity = file || folder + unless entity + e = Dav4rack::LockFailure.new + e.add_failure @path, MethodNotAllowed + raise e + end + begin + # If scope and type are not defined, the only thing we can + # logically assume is that the lock is being refreshed (office loves + # to do this for example, so we do a few checks, try to find the lock + # and ultimately extend it, otherwise we return Conflict for any failure + refresh = args && !args[:scope] && !args[:type] # Perhaps a lock refresh + if refresh + http_if = request.get_header('HTTP_IF') + if http_if.blank? + e = Dav4rack::LockFailure.new + e.add_failure @path, Conflict + raise e + end + l = nil + if http_if =~ /([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12})/ + l = DmsfLock.find_by(uuid: Regexp.last_match(1)) + end + unless l + e = Dav4rack::LockFailure.new + e.add_failure @path, Conflict + raise e + end + l.expires_at = 1.week.from_now + l.save! + @response['Lock-Token'] = l.uuid + return [1.week.to_i, l.uuid] + end + scope = :"scope_#{args[:scope] || 'exclusive'}" + type = :"type_#{args[:type] || 'write'}" + # l should be the instance of the lock we've just created + l = entity.lock!(scope, type, 1.week.from_now, args[:owner]) + @response['Lock-Token'] = l.uuid + [1.week.to_i, l.uuid] + rescue DmsfLockError => exception + e = Dav4rack::LockFailure.new(exception.message) + e.add_failure @path, Conflict + raise e + end + end + + # Unlock + # Token based unlock (authenticated) will ensure that a correct token is sent, further ensuring + # ownership of token before permitting unlock + def unlock(token) + return super unless exist? + + if token.blank? || (token == '<(null)>') || User.current.anonymous? + BadRequest + else + return BadRequest unless token =~ /([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12})/ + + token = Regexp.last_match(1) + l = DmsfLock.find_by(uuid: token) + return NoContent unless l + + # Additional case: if a user tries to unlock the file instead of the folder that's locked + # This should throw forbidden as only the lock at level initiated should be unlocked + entity = file || folder + return NoContent unless entity&.locked? + + l_entity = l.dmsf_file || l.dmsf_folder + if l_entity == entity + entity.unlock! + NoContent + else + Forbidden + end + end + end + + # HTTP POST request. + # Forbidden, as method should not be utilized. + def post(_request, _response) + raise Forbidden + end + + # HTTP PUT request. + def put(request) + raise BadRequest if collection? + raise Forbidden unless User.current.admin? || User.current.allowed_to?(:file_manipulation, project) + + unless !parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder, allow_system: false) + raise Forbidden + end + + return NoContent if ignore? + + reuse_revision = false + if exist? # We're over-writing something, so ultimately a new revision + f = file + # Disable versioning for file name patterns given in the plugin settings. + if basename.match(RedmineDmsf.dmsf_webdav_disable_versioning) + Rails.logger.info "Versioning disabled for #{basename}" + reuse_revision = true + end + reuse_revision = true if reuse_version_for_locked_file?(file) + last_revision = file.last_revision + if last_revision.size.zero? || reuse_revision + new_revision = last_revision + reuse_revision = true + else + if last_revision + new_revision = last_revision.dup + new_revision.reset_workflow + new_revision.source_revision = last_revision + else + new_revision = DmsfFileRevision.new + end + # Custom fields + last_revision.custom_field_values.each_with_index do |custom_value, i| + new_revision.custom_field_values[i].value = custom_value + end + end + unless reuse_revision + if new_revision.patch_version && (new_revision.patch_version != -32) + new_revision.increase_version(DmsfFileRevision::PATCH_VERSION) + elsif new_revision.minor_version && (new_revision.minor_version != -32) + new_revision.increase_version(DmsfFileRevision::MINOR_VERSION) + else + new_revision.increase_version(DmsfFileRevision::MAJOR_VERSION) + end + end + 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) + 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 + # to determine best approach to problem + new_revision.size = if request.body.respond_to?(:length) + request.body.length + elsif request.body.respond_to?(:size) + request.body.size + else + request.content_length # Bad Guess + end + + # Ignore 1b files sent for authentication + if RedmineDmsf.dmsf_webdav_ignore_1b_file_for_authentication? && new_revision.size == 1 + Rails.logger.warn "1b file '#{basename}' sent for authentication ignored" + return NoContent + end + + unless new_revision.valid? + Rails.logger.error new_revision.errors.full_messages.to_sentence + raise UnprocessableEntity + end + + unless f.save + Rails.logger.error f.errors.full_messages.to_sentence + 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 + new_revision.save + # Notifications + DmsfMailer.deliver_files_updated project, [f] + else + Rails.logger.error new_revision.errors.full_messages.to_sentence + raise InternalServerError + end + + Created + end + + # array of lock info hashes + # required keys are :time, :token, :depth + # other valid keys are :scope, :type, :root and :owner + def lockdiscovery + entity = file || folder + return [] unless entity&.locked? + + if entity.dmsf_folder&.locked? + entity.lock.reverse[0].dmsf_folder.locks # longwinded way of getting base items locks + else + entity.lock tree: false + end + end + + # returns an array of activelock ox elements + def lockdiscovery_xml + x = Nokogiri::XML::DocumentFragment.parse '' + Nokogiri::XML::Builder.with(x) do |doc| + doc.lockdiscovery do + lockdiscovery.each do |lock| + next if lock.expired? + + doc.activelock do + doc.locktype { doc.write } + doc.lockscope { lock.lock_scope == :scope_exclusive ? doc.exclusive : doc.shared } + doc.depth lock.dmsf_folder.nil? ? '0' : 'infinity' + doc.owner lock.user.to_s + if lock.expires_at.nil? + doc.timeout 'Infinite' + else + doc.timeout "Second-#{lock.expires_at.to_i - Time.current.to_i}" + end + lock_entity = lock.dmsf_folder || lock.dmsf_file + lock_path = "#{request.scheme}://#{request.host}:#{request.port}#{path_prefix}" + lock_path << "#{Addressable::URI.escape(lock_entity.project.identifier)}/" + pth = lock_entity.dmsf_path.map { |e| Addressable::URI.escape(e.respond_to?(:name) ? e.name : e.title) } + .join('/') + lock_path << pth + lock_path << '/' if lock_entity.is_a?(DmsfFolder) && lock_path[-1, 1] != '/' + doc.lockroot { doc.href lock_path } + if (lock.user.id == User.current.id) || User.current.allowed_to?(:force_file_unlock, project) + doc.locktoken { doc.href lock.uuid } + end + end + end + end + end + x + end + + private + + # Prepare file for download using Rack functionality: + # Download (see RedmineDmsf::Webdav::Download) extends Rack::File to allow single-file + # 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 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 + # file access, so we can properly keep logged information + if @request.env['HTTP_RANGE'].nil? + # Action + access = DmsfFileRevisionAccess.new + access.user = User.current + access.dmsf_file_revision = file.last_revision + access.action = DmsfFileRevisionAccess::DOWNLOAD_ACTION + access.save! + # Notification + begin + DmsfMailer.deliver_files_downloaded(@project, [file], @request.env['REMOTE_IP']) + rescue StandardError => e + Rails.logger.error "Could not send email notifications: #{e.message}" + end + end + File.new disk_file + end + + def reuse_version_for_locked_file?(file) + locks = file.lock + locks.each do |lock| + next if lock.expired? + # lock should be exclusive but just in case make sure we find this users lock + next if lock.user != User.current + + if lock.dmsf_file_last_revision_id.nil? || (lock.dmsf_file_last_revision_id < file.last_revision.id) + # At least one new revision has been created since the lock was created, reuse that revision. + return true + end + end + false + end + + def set_custom_property(element, value) + if value.present? + Redmine::Search.cache_store.write "#{property_key}-#{element[:name]}", value + else + Redmine::Search.cache_store.delete "#{property_key}-#{element[:name]}" + end + OK + end + + def custom_property(element) + val = Redmine::Search.cache_store.fetch "#{property_key}-#{element[:name]}" + val.presence || NotFound + end + + def property_key + if file + "DmsfFile-#{file.id}" + elsif folder + "DmsfFolder-#{folder.id}" + elsif subproject + "Project-#{subproject.id}" + else + "Project-#{project.id}" + end + end + + 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 + r = DmsfFileRevision.new + r.minor_version = 1 + r.major_version = 0 + r.title = DmsfFileRevision.filename_to_title(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) + return f + end + end + nil + end + + def ignore? + # Ignore file name patterns given in the plugin settings + if basename.match(RedmineDmsf.dmsf_webdav_ignore) + Rails.logger.info "#{basename} ignored" + return true + end + false + end + end + end +end diff --git a/lib/redmine_dmsf/webdav/index_resource.rb b/lib/redmine_dmsf/webdav/index_resource.rb new file mode 100644 index 00000000..b5812fdd --- /dev/null +++ b/lib/redmine_dmsf/webdav/index_resource.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Webdav + # Index resource + class IndexResource < BaseResource + def children + unless @children + @children = [] + load_projects Project.where(parent_id: nil) + end + @children + end + + def collection? + true + end + + def creation_date + Time.current + end + + def last_modified + Time.current + end + + # Index resource ALWAYS exists + def exist? + true + end + + def etag + format '%x-%x-%x', inode: children.count, size: 4096, modified: Time.current.to_i + end + + def content_type + 'inode/directory' + end + + def content_length + 4096 + end + + def get(_request, response) + html_display + response['Content-Length'] = response.body.bytesize.to_s + response['Content-Type'] = 'text/html' + OK + end + end + end +end diff --git a/lib/redmine_dmsf/webdav/project_resource.rb b/lib/redmine_dmsf/webdav/project_resource.rb new file mode 100644 index 00000000..a530f78d --- /dev/null +++ b/lib/redmine_dmsf/webdav/project_resource.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Webdav + # Project resource + class ProjectResource < BaseResource + include Redmine::I18n + + def children + return @children if @children + + @children = [] + return @children unless project + + # Sub-projects + load_projects(project.children) if RedmineDmsf.dmsf_projects_as_subfolders? + return @children unless project.module_enabled?(:dmsf) + + # Folders + if User.current.allowed_to?(:view_dmsf_folders, project) + project.dmsf_folders.visible.pluck(:title).each do |title| + @children.push child(title) + end + end + # Files + if User.current.allowed_to?(:view_dmsf_files, project) + project.dmsf_files.visible.pluck(:name).each do |name| + @children.push child(name) + end + end + @children + end + + def exist? + project&.visible? + end + + def collection? + true + end + + def creation_date + project&.created_on + end + + def last_modified + project&.updated_on + end + + def etag + format '%x-%x-%x', + inode: 0, size: 4096, modified: (last_modified ? last_modified.to_i : 0) + end + + def name + ProjectResource.create_project_name(project) + end + + def long_name + "[#{project&.name}]" + end + + def content_type + 'inode/directory' + end + + def special_type + l(:field_project) + end + + def content_length + 4096 + end + + def get(_request, response) + html_display + response['Content-Length'] = response.body.bytesize.to_s + response['Content-Type'] = 'text/html' + OK + end + + def make_collection + MethodNotAllowed + end + + def move(_dest) + MethodNotAllowed + end + + def delete + MethodNotAllowed + end + + def lock(_args) + e = Dav4rack::LockFailure.new + e.add_failure @path, MethodNotAllowed + raise e + end + + def self.create_project_name(prj) + return unless prj + + if RedmineDmsf.dmsf_webdav_use_project_names? + "[#{DmsfFolder.get_valid_title(prj.name)} #{prj.id}]" + else + "[#{prj.identifier}]" + end + end + end + end +end diff --git a/lib/redmine_dmsf/webdav/resource_proxy.rb b/lib/redmine_dmsf/webdav/resource_proxy.rb new file mode 100644 index 00000000..57977452 --- /dev/null +++ b/lib/redmine_dmsf/webdav/resource_proxy.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Webdav + # ResourceProxy + # + # This is more of a factory approach of an object, class determines which class to + # instantiate based on pathing information, it then populates @resource_c with this + # object, and proxies calls made against class to it. + class ResourceProxy < Dav4rack::Resource + attr_reader :read_only + + delegate :propstats, to: :@resource_c + delegate :set_property, to: :@resource_c + delegate :options, to: :@resource_c + delegate :lockdiscovery, to: :@resource_c + delegate :lockdiscovery_xml, to: :@resource_c + delegate :children, to: :@resource_c + delegate :collection?, to: :@resource_c + delegate :exist?, to: :@resource_c + delegate :creation_date, to: :@resource_c + delegate :last_modified, to: :@resource_c + delegate :etag, to: :@resource_c + delegate :content_type, to: :@resource_c + delegate :content_length, to: :@resource_c + delegate :get, to: :@resource_c + delegate :special_type, to: :@resource_c + delegate :name, to: :@resource_c + delegate :long_name, to: :@resource_c + delegate :get_property, to: :@resource_c + delegate :remove_property, to: :@resource_c + delegate :properties, to: :@resource_c + + def initialize(path, request, response, options) + # Check the settings cache for each request + Setting.check_cache + # Return 404 - NotFound if WebDAV is not enabled + raise NotFound unless RedmineDmsf.dmsf_webdav? + + super + rc = get_resource_class(path) + @resource_c = rc.new(path, request, response, options) + @resource_c.accessor = self if @resource_c + @read_only = RedmineDmsf.dmsf_webdav_strategy == 'WEBDAV_READ_ONLY' + end + + def authenticate?(username, password) + User.current = User.try_to_login(username, password) + User.current && !User.current.anonymous? + end + + def supports_locking? + !@read_only + end + + def put(request) + raise BadGateway if @read_only + + @resource_c.put request + end + + def delete + raise BadGateway if @read_only + + @resource_c.delete + end + + def copy(dest) + raise BadGateway if @read_only + + @resource_c.copy dest + end + + def move(dest) + raise BadGateway if @read_only + + @resource_c.move dest + end + + def make_collection + raise BadGateway if @read_only + + @resource_c.make_collection + end + + def lock(args) + raise BadGateway if @read_only + + @resource_c.lock args + end + + def lock_check(lock_scope = nil) + @resource_c.lock_check lock_scope + end + + def unlock(token) + raise BadGateway if @read_only + + @resource_c.unlock token + end + + def resource + @resource_c + end + + # Adds the given xml namespace to namespaces and returns the prefix + def add_namespace(namespace, _prefix = '') + return if namespace.blank? + + prefix = 'ns1' + 2.step do |i| + break unless namespaces.value?(prefix) + + prefix = "ns#{i}" + end + namespaces[namespace] = prefix + prefix + end + + # returns the prefix for the given namespace, adding it if necessary + def prefix_for(ns_href) + namespaces[ns_href] || add_namespace(ns_href) + end + + def authentication_realm + RedmineDmsf::Webdav::AUTHENTICATION_REALM + end + + private + + def get_resource_class(path) + pinfo = path.split('/').drop(1) + return IndexResource if pinfo.empty? + + return ProjectResource if pinfo.length == 1 + + i = 1 + project = nil + prj = nil + while pinfo.length.positive? + prj = BaseResource.get_project(Project, pinfo.first, project) + break unless prj + + project = prj + i += 1 + pinfo = path.split('/').drop(i) + end + prj ? ProjectResource : DmsfResource + end + end + end +end diff --git a/lib/tasks/dmsf_alert_approvals.rake b/lib/tasks/dmsf_alert_approvals.rake new file mode 100644 index 00000000..de7d9386 --- /dev/null +++ b/lib/tasks/dmsf_alert_approvals.rake @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +desc <<~END_DESC + Alert all users who are expected to do an approval in the current approval steps + + Available options: + * dry_run - No email, just print list of recipients to the console + + Example: + rake redmine:dmsf_alert_approvals RAILS_ENV="production" + rake redmine:dmsf_alert_approvals dry_run=1 RAILS_ENV="production" +END_DESC + +namespace :redmine do + task dmsf_alert_approvals: :environment do + DmsfAlertApprovals.alert + end +end + +# Alert approvals +class DmsfAlertApprovals + def self.alert + dry_run = ENV.fetch('dry_run', nil) + revisions = DmsfFileRevision.visible.joins(:dmsf_file) + .joins('JOIN projects ON projects.id = dmsf_files.project_id') + .where(dmsf_file_revisions: { workflow: DmsfWorkflow::STATE_WAITING_FOR_APPROVAL }, + projects: { status: Project::STATUS_ACTIVE }) + revisions.each do |revision| + next unless revision.dmsf_file.last_revision == revision + + workflow = DmsfWorkflow.find_by(id: revision.dmsf_workflow_id) + next unless workflow + + assignments = workflow.next_assignments revision.id + assignments.each do |assignment| + next unless assignment.user.active? + + if dry_run + $stdout.puts "#{assignment.user.name} <#{assignment.user.mail}>" + else + DmsfMailer.deliver_workflow_notification( + [assignment.user], + workflow, + revision, + :text_email_subject_requires_approval, + :text_email_finished_step, + :text_email_to_proceed, + nil, + assignment.dmsf_workflow_step + ) + end + end + end + end +end diff --git a/lib/tasks/dmsf_convert_documents.rake b/lib/tasks/dmsf_convert_documents.rake new file mode 100644 index 00000000..1aec2e6f --- /dev/null +++ b/lib/tasks/dmsf_convert_documents.rake @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# 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 +# . + +desc <<~END_DESC + Convert projects' Documents to DMSF folder/file structure. + + Converted project must have Document module enabled + + 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" + rake redmine:dmsf_convert_documents project=test dry_run=1 RAILS_ENV="production" + rake redmine:dmsf_convert_documents project=test issues=1 RAILS_ENV="production" +END_DESC + +# Convert documents +class DmsfConvertDocuments + def initialize + @dry_run = ENV.fetch('dry_run', nil) + @projects = [] + if ENV['project'] + p = Project.find(ENV['project']) + @projects << p if p&.active? + else + @projects.concat(Project.active.to_a) + end + @issues = ENV.fetch('issues', nil) + end + + def convert_projects + if @projects.any? + @projects.each do |project| + $stdout.puts "Processing project: #{project.name}" + convert_documents(project) if project.module_enabled?('documents') + convert_issues(project) if @issues && project.module_enabled?('issue_tracking') + end + else + warn 'No active projects found.' + end + end + + def convert_issues(project) + $stdout.puts 'Issues' + unless Setting.plugin_redmine_dmsf['dmsf_act_as_attachable'] + warn "'Act as attachable' must be checked in the plugin's settings" + return + end + project.issues.each do |issue| + next unless issue.attachments.any? + + $stdout.puts "Processing: #{issue}" + # - folder + if @dry_run + $stdout.puts "Dry run #{issue.id} - #{DmsfFolder.get_valid_title(issue.subject)} folder" + else + project.enable_module!('dmsf') + $stdout.puts "Creating #{issue.id} - #{DmsfFolder.get_valid_title(issue.subject)} folder" + folder = issue.system_folder(create: true, prj_id: project.id) + end + files = [] + attachments = [] + issue.attachments.each do |attachment| + @fail = false + create_document_from_attachment(project, folder, attachment, files, issue) + attachments << attachment unless @fail + end + next if @dry_run + + attachments.each do |attachment| + issue.init_journal User.anonymous + issue.attachments.delete attachment + end + end + end + + def convert_documents(project) + @fail = false + folders = [] + if project.documents.any? + $stdout.puts 'Documents' + project.enable_module!('dmsf') unless @dry_run + project.documents.each do |document| + $stdout.puts "Processing document: #{document.title}" + folder = DmsfFolder.new + folder.project = project + attachment = document.attachments.reorder(created_on: :asc).first + folder.user = attachment ? attachment.author : User.active.where(admin: true).first + folder.title = DmsfFolder.get_valid_title(document.title) + i = 1 + suffix = '' + while folders.index { |f| f.title == (folder.title + suffix) } + i += 1 + suffix = "_#{i}" + end + folder.title = folder.title + suffix + folder.description = document.description + if @dry_run + $stdout.puts "Dry run folder: #{folder.title}" + warn(folder.errors.full_messages.to_sentence) if folder.invalid? + else + begin + folder.save! + $stdout.puts "Created folder: #{folder.title}" + rescue StandardError => e + warn "Creating folder: #{folder.title} failed" + warn e.message + @fail = true + next + end + end + folders << folder + files = [] + @fail = false + document.attachments.each do |a| + create_document_from_attachment(project, folder, a, files, document) + end + document.destroy unless @dry_run || @fail + end + end + project.disable_module!('documents') unless @dry_run || @fail + end + + private + + def create_document_from_attachment(project, folder, attachment, files, container) + file = DmsfFile.new + file.project_id = project.id + file.dmsf_folder = folder + file.name = attachment.filename + i = 1 + suffix = '' + filename = DmsfFileRevision.remove_extension(file.name) + extname = File.extname(file.name) + while files.index { |f| f.name == "#{filename}#{suffix}#{extname}" } + i += 1 + suffix = "_#{i}" + end + # Need to save file first to generate id for it in case of creation. + # File id is needed to properly generate revision disk filename + file.name = DmsfFileRevision.remove_extension(file.name) + suffix + File.extname(file.name) + unless File.exist?(attachment.diskfile) + warn "Creating file: #{attachment.filename} failed, attachment file #{attachment.diskfile} doesn't exist" + @fail = true + return + end + if @dry_run + file.id = attachment.id # Just to have an ID there + $stdout.puts "Dry run file: #{file.name}" + warn(file.errors.full_messages.to_sentence) if file.invalid? + else + file.save! + end + revision = DmsfFileRevision.new + revision.dmsf_file = file + revision.name = file.name + revision.title = DmsfFileRevision.filename_to_title(attachment.filename) + revision.description = attachment.description + revision.user = attachment.author + revision.created_at = attachment.created_on + revision.updated_at = attachment.created_on + revision.major_version = 0 + revision.minor_version = 1 + revision.comment = "Converted from #{container.class.name}" + revision.mime_type = attachment.content_type + revision.disk_filename = revision.new_storage_filename + if @dry_run + $stdout.puts "Dry run revision: #{revision.title}" + warn(revision.errors.full_messages.to_sentence) if revision.invalid? + else + FileUtils.cp attachment.diskfile, revision.disk_file(search_if_not_exists: false) + revision.size = File.size(revision.disk_file(search_if_not_exists: false)) + revision.save! + end + files << file + unless @dry_run + attachment.destroy + $stdout.puts "Created file: #{file.name}" + end + rescue StandardError => e + warn "Creating file: #{attachment.filename} failed" + warn e.message + @fail = true + end +end + +namespace :redmine do + task dmsf_convert_documents: :environment do + convert = DmsfConvertDocuments.new + convert.convert_projects + end +end diff --git a/lib/tasks/dmsf_create_digests.rake b/lib/tasks/dmsf_create_digests.rake new file mode 100644 index 00000000..12bf25ca --- /dev/null +++ b/lib/tasks/dmsf_create_digests.rake @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +desc <<~END_DESC + DMSF maintenance task + * Create missing checksums for all file 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" +END_DESC + +namespace :redmine do + task dmsf_create_digests: :environment do + m = DmsfCreateDigest.new + m.dmsf_create_digests + end +end + +# Create digest +class DmsfCreateDigest + def initialize + @dry_run = ENV.fetch('dry_run', nil) + @force_sha256 = ENV.fetch('forceSHA256', nil) + end + + def dmsf_create_digests + # Checksum is always the same via WebDAV #1384 + revisions = DmsfFileRevision.where(['digest IS NULL OR digest = ? OR length(digest) < ?', + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + @force_sha256 ? 64 : 32]) + count = revisions.all.size + n = 0 + revisions.each_with_index do |rev, i| + if File.exist?(rev.disk_file) + file = File.new rev.disk_file, 'r' + if file.respond_to?(:read) + sha = Digest::SHA256.new + while (buffer = file.read(8192)) + sha.update buffer + end + rev.digest = sha.hexdigest + else + rev.digest = Digest::SHA256.file(rev.disk_file) + end + rev.save unless @dry_run + else + puts "#{rev.disk_file} not found" + end + n += 1 + # Progress bar + print "\r#{i * 100 / count}%" + end + print "\r100%\n" + # Result + $stdout.puts "#{n}/#{count} revisions updated." + end +end diff --git a/lib/tasks/dmsf_maintenance.rake b/lib/tasks/dmsf_maintenance.rake new file mode 100644 index 00000000..b6ed4ff4 --- /dev/null +++ b/lib/tasks/dmsf_maintenance.rake @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +desc <<~END_DESC + DMSF maintenance task + * 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) + * Report all documents without a corresponding file in the file system (dry_run only) + + 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" +END_DESC + +namespace :redmine do + task dmsf_maintenance: :environment do + m = DmsfMaintenance.new + begin + $stdout.puts "\n" + Dir.chdir DmsfFile.storage_path.to_s + $stdout.puts "Files...\n" + m.files + $stdout.puts "Documents...\n" + m.documents + if m.dry_run + m.result + else + m.clean + end + rescue StandardError => e + warn e.message + end + end +end + +# Maintenance +class DmsfMaintenance + include ActionView::Helpers::NumberHelper + + attr_accessor :dry_run + + def initialize + @dry_run = ENV.fetch('dry_run', nil) + @files_to_delete = [] + @documents_to_delete = [] + end + + def files + Dir.glob('**/*').each do |f| + check_file(f) unless Dir.exist?(f) + end + end + + def documents + DmsfFile.find_each do |f| + r = f.last_revision + if r.nil? || !File.exist?(r.disk_file) + @documents_to_delete << f + $stdout.puts "\t#{r.disk_file}\n" if r + end + end + end + + def result + # Files + size = 0 + @files_to_delete.each { |f| size += File.size(f) } + $stdout.puts "\n#{@files_to_delete.count} files haven't got a corresponding revision and can be deleted." + $stdout.puts "#{number_to_human_size(size)} can be released.\n\n" + # Links + size = DmsfLink.where(project_id: -1).count + $stdout.puts "#{size} links can be deleted.\n\n" + # Documents + $stdout.puts "#{@documents_to_delete.size} corrupted documents.\n\n" + end + + def clean + # Files + size = 0 + @files_to_delete.each do |f| + size += File.size(f) + File.delete f + end + $stdout.puts "\n#{@files_to_delete.count} files hadn't got a coresponding revision and have been be deleted." + $stdout.puts "#{number_to_human_size(size)} has been released\n\n" + # Links + size = DmsfLink.where(project_id: -1).count + DmsfLink.where(project_id: -1).delete_all + $stdout.puts "#{size} links have been deleted.\n\n" + end + + private + + def check_file(file) + name = Pathname.new(file).basename.to_s + if name.match?(/^\d+_\d+_.*/) + n = DmsfFileRevision.where(disk_filename: name).count + unless n.positive? + @files_to_delete << file + $stdout.puts "\t#{file}\t#{number_to_human_size(File.size(file))}" + end + else + warn "\t#{file} doesn't seem to be a DMSF file!" + end + end +end diff --git a/lib/tasks/dmsf_webdav_test.rake b/lib/tasks/dmsf_webdav_test.rake new file mode 100644 index 00000000..f57b533c --- /dev/null +++ b/lib/tasks/dmsf_webdav_test.rake @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +desc <<~END_DESC + DMSF WebDAV test task + * Create a test project with DMSF module enabled + * Enable REST API in settings + * Enable WebDAV in plugin's settings + + Example: + rake redmine:dmsf_webdav_test_on RAILS_ENV="test" + rake redmine:dmsf_webdav_test_off RAILS_ENV="test" +END_DESC + +namespace :redmine do + task dmsf_webdav_test_on: :environment do + prj = Project.new + prj.identifier = 'dmsf_test_project' + prj.name = 'DMSF Test Project' + prj.description = 'A temporary project for Litmus tests' + prj.enable_module! :dmsf + Rails.logger.error(prj.errors.full_messages.to_sentence) unless prj.save + # Settings + Setting.rest_api_enabled = true + # Plugin's settings + plugin_settings = ActiveSupport::HashWithIndifferentAccess.new + plugin_settings['dmsf_webdav'] = '1' + plugin_settings['dmsf_webdav_strategy'] = 'WEBDAV_READ_WRITE' + plugin_settings['dmsf_storage_directory'] = File.join('files', ['dmsf']) + plugin_settings['dmsf_webdav_authentication'] = 'Basic' + Setting.plugin_redmine_dmsf = plugin_settings + end + + task dmsf_webdav_test_off: :environment do + # Settings + Setting.rest_api_enabled = false + # Plugin's settings + Setting.plugin_redmine_dmsf = nil + prj = Project.find_by(identifier: 'dmsf_test_project') + prj&.delete + end +end diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml new file mode 100644 index 00000000..5eaeb70c --- /dev/null +++ b/test/fixtures/custom_fields.yml @@ -0,0 +1,18 @@ +--- +cf_1: + name: Tag + min_length: 0 + regexp: "" + is_for_all: true + is_filter: true + type: DmsfFileRevisionCustomField + max_length: 0 + possible_values: + - User documentation + - Technical documentation + id: 21 + is_required: false + field_format: list + default_value: '' + editable: false + position: 1 diff --git a/test/fixtures/custom_values.yml b/test/fixtures/custom_values.yml new file mode 100644 index 00000000..a251184e --- /dev/null +++ b/test/fixtures/custom_values.yml @@ -0,0 +1,13 @@ +--- +cv_1: + customized_type: DmsfFolder + custom_field_id: 21 + customized_id: 1 + id: 21 + value: 'User documentation' +cv_2: + customized_type: DmsfFileRevision + custom_field_id: 21 + customized_id: 1 + id: 22 + value: 'Technical documentation' diff --git a/test/fixtures/dmsf_file_revisions.yml b/test/fixtures/dmsf_file_revisions.yml new file mode 100644 index 00000000..60344fa0 --- /dev/null +++ b/test/fixtures/dmsf_file_revisions.yml @@ -0,0 +1,287 @@ +--- +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 + title: "Test File" + description: 'Some file :-)' + workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + 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 +dmsf_file_revisions_002: + id: 2 + dmsf_file_id: 2 + source_dmsf_file_revision_id: NULL + name: "test2.txt" + disk_filename: "test2.txt" + size: 4 + mime_type: text/plain + title: "Test File" + description: NULL + workflow: NULL + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + user_id: 1 + dmsf_workflow_assigned_by_user_id: NULL + dmsf_workflow_started_by_user_id: NULL + digest: '81dc9bdb52d04dc20036dbd8313ed055' + created_at: 2017-04-18 14:52:27 +02:00 + +#revision for deleted file on dmsf-enabled project +dmsf_file_revisions_003: + id: 3 + dmsf_file_id: 3 + source_dmsf_file_revision_id: NULL + name: 'deleted.txt' + disk_filename: 'deleted.txt' + size: 4 + mime_type: 'text/plain' + title: 'Test File' + description: NULL + workflow: NULL + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 1 + deleted_by_user_id: 1 + 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: + id: 4 + dmsf_file_id: 4 + source_dmsf_file_revision_id: NULL + name: 'test4.txt' + disk_filename: 'test4.txt' + size: 4 + mime_type: 'text/plain' + title: 'Test File' + description: NULL + workflow: NULL + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + user_id: 1 + dmsf_workflow_assigned_by_user_id: NULL + dmsf_workflow_started_by_user_id: NULL + digest: '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' + title: 'Test image' + description: NULL + workflow: NULL + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + user_id: 1 + dmsf_workflow_assigned_by_user_id: NULL + dmsf_workflow_started_by_user_id: NULL + digest: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3' + created_at: 2017-04-18 14:52:27 +02:00 + +dmsf_file_revisions_007: + id: 7 + dmsf_file_id: 8 + source_dmsf_file_revision_id: NULL + name: 'test.pdf' + disk_filename: 'test.pdf' + size: 4 + mime_type: 'application/pdf' + title: 'Test PDF' + description: NULL + workflow: NULL + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + user_id: 1 + dmsf_workflow_assigned_by_user_id: NULL + dmsf_workflow_started_by_user_id: NULL + digest: '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 + size: 0 + mime_type: 'text/plain' + title: 'My File' + description: NULL + workflow: NULL + 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-18 14:52:27 +02:00 + +dmsf_file_revisions_009: + id: 9 + 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 + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + user_id: 1 + dmsf_workflow_assigned_by_user_id: NULL + dmsf_workflow_started_by_user_id: NULL + digest: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + created_at: 2017-04-18 14:52:27 +02:00 + +dmsf_file_revisions_010: + id: 10 + 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 + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + 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: + id: 11 + 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 + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + 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: + id: 12 + 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 + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + user_id: 1 + dmsf_workflow_assigned_by_user_id: NULL + dmsf_workflow_started_by_user_id: NULL + digest: '81dc9bdb52d04dc20036dbd8313ed055' + created_at: 2022-02-03 13:39:27 +02:00 + +dmsf_file_revisions_013: + id: 13 + dmsf_file_id: 13 + source_dmsf_file_revision_id: NULL + name: 'test.odt' + disk_filename: 'test.odt' + size: 4 + mime_type: 'application/vnd.oasis.opendocument.text' + title: 'Test office document' + description: 'LibreOffice text' + workflow: 0 + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + user_id: 1 + dmsf_workflow_assigned_by_user_id: NULL + dmsf_workflow_started_by_user_id: NULL + digest: '89a6d0ea9aafc21a978152f3e4977812d5d7d623505749471f256a90fc7c5f72' + created_at: 2017-04-01 08:54:00 +02:00 \ No newline at end of file diff --git a/test/fixtures/dmsf_files.yml b/test/fixtures/dmsf_files.yml new file mode 100644 index 00000000..e6e6f86c --- /dev/null +++ b/test/fixtures/dmsf_files.yml @@ -0,0 +1,118 @@ +--- +dmsf_files_001: + id: 1 + project_id: 1 + dmsf_folder_id: NULL + name: 'test.txt' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +#file on non-dmsf enabled project +dmsf_files_002: + id: 2 + project_id: 2 + dmsf_folder_id: NULL + name: 'test.txt' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +#deleted file on dmsf-enabled project +dmsf_files_003: + id: 3 + project_id: 1 + dmsf_folder_id: NULL + name: 'deleted.txt' + notification: false + deleted: 1 + deleted_by_user_id: 1 + +dmsf_files_004: + id: 4 + project_id: 1 + dmsf_folder_id: 2 + name: 'test.txt' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_005: + id: 5 + project_id: 1 + dmsf_folder_id: 5 + name: 'test.txt' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_006: + id: 6 + project_id: 2 + dmsf_folder_id: 3 + name: 'test.mp4' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_007: + id: 7 + project_id: 1 + dmsf_folder_id: 8 + name: 'test.gif' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_008: + id: 8 + project_id: 1 + dmsf_folder_id: NULL + name: 'test.pdf' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_009: + id: 9 + project_id: 1 + dmsf_folder_id: NULL + name: 'myfile.txt' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_010: + id: 10 + project_id: 1 + dmsf_folder_id: NULL + name: 'zero.txt' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_011: + id: 11 + project_id: 1 + dmsf_folder_id: 9 + name: 'zero.txt' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_012: + id: 12 + project_id: 5 + dmsf_folder_id: NULL + name: 'test.txt' + notification: false + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_013: + id: 13 + project_id: 1 + dmsf_folder_id: NULL + name: 'test.odt' + deleted: 0 + deleted_by_user_id: NULL \ No newline at end of file diff --git a/test/fixtures/dmsf_folder_permissions.yml b/test/fixtures/dmsf_folder_permissions.yml new file mode 100644 index 00000000..be652300 --- /dev/null +++ b/test/fixtures/dmsf_folder_permissions.yml @@ -0,0 +1,12 @@ +--- +dmsf_folder_permissions_001: + id: 1 + dmsf_folder_id: 7 + object_id: 1 + object_type: 'Role' + +dmsf_folder_permissions_002: + id: 2 + dmsf_folder_id: 7 + object_id: 3 + object_type: 'User' diff --git a/test/fixtures/dmsf_folders.yml b/test/fixtures/dmsf_folders.yml new file mode 100644 index 00000000..752db202 --- /dev/null +++ b/test/fixtures/dmsf_folders.yml @@ -0,0 +1,72 @@ +--- +dmsf_folders_001: + id: 1 + title: folder1 + project_id: 1 + dmsf_folder_id: NULL + user_id: 1 + +dmsf_folders_002: + id: 2 + title: folder2 + project_id: 1 + dmsf_folder_id: 1 + user_id: 1 + +dmsf_folders_003: + id: 3 + title: folder3 + project_id: 2 + dmsf_folder_id: NULL + user_id: 1 + +dmsf_folders_004: + id: 4 + title: folder2 + project_id: 2 + dmsf_folder_id: 3 + user_id: 1 + +dmsf_folders_005: + id: 5 + title: folder5 + project_id: 1 + dmsf_folder_id: 2 + user_id: 1 + +dmsf_folders_006: + id: 6 + title: folder6 + project_id: 1 + dmsf_folder_id: NULL + user_id: 2 + +dmsf_folders_007: + id: 7 + title: folder7 + project_id: 1 + dmsf_folder_id: NULL + user_id: 1 + +dmsf_folders_008: + id: 8 + title: .Issues + project_id: 1 + dmsf_folder_id: NULL + user_id: 1 + system: true + +dmsf_folders_009: + id: 9 + title: 1 - Cannot print recipes + project_id: 1 + dmsf_folder_id: 8 + user_id: 1 + system: true + +dmsf_folders_010: + id: 10 + title: folder10 + project_id: 5 + dmsf_folder_id: NULL + user_id: 1 \ No newline at end of file diff --git a/test/fixtures/dmsf_links.yml b/test/fixtures/dmsf_links.yml new file mode 100644 index 00000000..41095cdd --- /dev/null +++ b/test/fixtures/dmsf_links.yml @@ -0,0 +1,92 @@ +--- +folder_link: + id: 1 + target_project_id: 1 + target_id: 1 + target_type: DmsfFolder + name: folder1_link + project_id: 1 + dmsf_folder_id: NULL + deleted: 0 + deleted_by_user_id: NULL + created_at: <%= DateTime.current %> + updated_at: <%= DateTime.current %> + +file_link: + id: 2 + target_project_id: 1 + target_id: 4 + target_type: DmsfFile + name: test_link + project_id: 1 + dmsf_folder_id: 1 + deleted: 0 + deleted_by_user_id: NULL + created_at: <%= DateTime.current %> + updated_at: <%= DateTime.current %> + user_id: 1 + +folder_link2: + id: 3 + target_project_id: 2 + target_id: 4 + target_type: DmsfFolder + name: folder4_link + project_id: 2 + dmsf_folder_id: NULL + deleted: 0 + deleted_by_user_id: NULL + created_at: <%= DateTime.current %> + updated_at: <%= DateTime.current %> + +file_link2: + id: 4 + target_project_id: 1 + target_id: 4 + target_type: DmsfFile + name: test_link2 + project_id: 1 + dmsf_folder_id: NULL + deleted: 0 + deleted_by_user_id: NULL + created_at: <%= DateTime.current %> + updated_at: <%= DateTime.current %> + +url_link: + id: 5 + target_project_id: 1 + target_type: DmsfUrl + name: url_link + project_id: 1 + dmsf_folder_id: 1 + external_url: 'https://www.kontron.com' + deleted: 0 + deleted_by_user_id: NULL + created_at: <%= DateTime.current %> + updated_at: <%= DateTime.current %> + +file_link3: + id: 6 + target_project_id: 1 + target_id: 1 + target_type: DmsfFile + name: file1_link + project_id: 1 + dmsf_folder_id: NULL + deleted: 0 + deleted_by_user_id: NULL + created_at: <%= DateTime.current %> + updated_at: <%= DateTime.current %> + +file_link4: + id: 7 + target_project_id: 1 + target_id: 1 + target_type: DmsfFile + name: file1_link + project_id: 1 + dmsf_folder_id: 9 + deleted: 0 + deleted_by_user_id: NULL + created_at: <%= DateTime.current %> + updated_at: <%= DateTime.current %> \ No newline at end of file diff --git a/test/fixtures/dmsf_locks.yml b/test/fixtures/dmsf_locks.yml new file mode 100644 index 00000000..53b27bf7 --- /dev/null +++ b/test/fixtures/dmsf_locks.yml @@ -0,0 +1,18 @@ +--- +dmsf_locks_001: + id: 1 + entity_id: 2 + user_id: 1 # admin + entity_type: 0 # DmsfFile + lock_type_cd: 0 + lock_scope_cd: 0 + uuid: <%= UUIDTools::UUID.random_create.to_s %> + +dmsf_locks_002: + id: 2 + entity_id: 2 + user_id: 2 # jsmith + entity_type: 1 # DmsfFolder + lock_type_cd: 0 + lock_scope_cd: 0 + uuid: <%= UUIDTools::UUID.random_create.to_s %> diff --git a/test/fixtures/dmsf_public_urls.yml b/test/fixtures/dmsf_public_urls.yml new file mode 100644 index 00000000..e1f97ba4 --- /dev/null +++ b/test/fixtures/dmsf_public_urls.yml @@ -0,0 +1,18 @@ +--- +public_url_1: + id: 1 + dmsf_file_id: 1 + user_id: 1 + token: 'd8d33e21914a433b280fdc94450ee212' + expire_at: <%= (Time.current + 1.day).to_date %> + created_at: <%= DateTime.current %> + updated_at: <%= DateTime.current %> + +public_url_2: + id: 2 + dmsf_file_id: 1 + user_id: 1 + token: 'e8d33e21914a433b280fdc94450ee212' + expire_at: <%= (Time.current - 1.day).to_date %> + created_at: <%= DateTime.current %> + updated_at: <%= DateTime.current %> diff --git a/test/fixtures/dmsf_workflow_step_actions.yml b/test/fixtures/dmsf_workflow_step_actions.yml new file mode 100644 index 00000000..4f43948a --- /dev/null +++ b/test/fixtures/dmsf_workflow_step_actions.yml @@ -0,0 +1,40 @@ +--- +wfsac1: + id: 1 + dmsf_workflow_step_assignment_id: 1 + action: 1 + note: 'Approval' + created_at: '2013-05-03 10:45:35' + author_id: 2 + +wfsac2: + id: 2 + dmsf_workflow_step_assignment_id: 5 + action: 1 + note: 'Approval' + created_at: '2013-05-03 10:45:35' + author_id: 2 + +wfsac3: + id: 3 + dmsf_workflow_step_assignment_id: 6 + action: 1 + note: 'Approval' + created_at: '2013-05-03 10:45:35' + author_id: 2 + +wfsac4: + id: 4 + dmsf_workflow_step_assignment_id: 7 + action: 1 + note: 'Approval' + created_at: '2013-05-03 10:45:35' + author_id: 2 + +wfsac5: + id: 5 + dmsf_workflow_step_assignment_id: 8 + action: 1 + note: 'Approval' + created_at: '2013-05-03 10:45:35' + author_id: 2 \ No newline at end of file diff --git a/test/fixtures/dmsf_workflow_step_assignments.yml b/test/fixtures/dmsf_workflow_step_assignments.yml new file mode 100644 index 00000000..c8e0760b --- /dev/null +++ b/test/fixtures/dmsf_workflow_step_assignments.yml @@ -0,0 +1,54 @@ +--- +wfsa1: + id: 1 + dmsf_workflow_step_id: 1 + user_id: 1 + dmsf_file_revision_id: 2 + +wfsa2: + id: 2 + dmsf_workflow_step_id: 4 + user_id: 2 + dmsf_file_revision_id: 2 + +wfsa3: + id: 3 + dmsf_workflow_step_id: 2 + user_id: 1 + dmsf_file_revision_id: 2 + +wfsa4: + id: 4 + dmsf_workflow_step_id: 3 + user_id: 2 + dmsf_file_revision_id: 2 + +wfsa5: + id: 5 + dmsf_workflow_step_id: 1 + user_id: 1 + dmsf_file_revision_id: 1 + +wfsa6: + id: 6 + dmsf_workflow_step_id: 4 + user_id: 2 + dmsf_file_revision_id: 1 + +wfsa7: + id: 7 + dmsf_workflow_step_id: 2 + user_id: 2 + dmsf_file_revision_id: 1 + +wfsa8: + id: 8 + dmsf_workflow_step_id: 3 + user_id: 1 + dmsf_file_revision_id: 1 + +wfsa9: + id: 9 + dmsf_workflow_step_id: 5 + user_id: 2 + dmsf_file_revision_id: 1 \ No newline at end of file diff --git a/test/fixtures/dmsf_workflow_steps.yml b/test/fixtures/dmsf_workflow_steps.yml new file mode 100644 index 00000000..9d5152ac --- /dev/null +++ b/test/fixtures/dmsf_workflow_steps.yml @@ -0,0 +1,39 @@ +--- + +wfs1: + id: 1 + dmsf_workflow_id: 1 + step: 1 + name: '1st step' + user_id: 1 + operator: 0 + +wfs2: + id: 2 + dmsf_workflow_id: 1 + step: 2 + name: '2nd step' + user_id: 2 + operator: 1 + +wfs3: + id: 3 + dmsf_workflow_id: 1 + step: 2 + user_id: 1 + operator: 1 + +wfs4: + id: 4 + dmsf_workflow_id: 1 + step: 1 + user_id: 2 + operator: 1 + +wfs5: + id: 5 + dmsf_workflow_id: 1 + step: 3 + name: '3rd step' + user_id: 2 + operator: 1 diff --git a/test/fixtures/dmsf_workflows.yml b/test/fixtures/dmsf_workflows.yml new file mode 100644 index 00000000..ed0981cb --- /dev/null +++ b/test/fixtures/dmsf_workflows.yml @@ -0,0 +1,14 @@ +--- +wf1: + id: 1 + name: wf1 + project_id: 1 + +wf2: + id: 2 + name: wf2 + +wf3: + id: 3 + name: wf3 + status: 0 \ No newline at end of file diff --git a/test/fixtures/files/2017/04/deleted.txt b/test/fixtures/files/2017/04/deleted.txt new file mode 100644 index 00000000..d800886d --- /dev/null +++ b/test/fixtures/files/2017/04/deleted.txt @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/test/fixtures/files/2017/04/test.gif b/test/fixtures/files/2017/04/test.gif new file mode 100644 index 00000000..d800886d --- /dev/null +++ b/test/fixtures/files/2017/04/test.gif @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/test/fixtures/files/2017/04/test.mp4 b/test/fixtures/files/2017/04/test.mp4 new file mode 100644 index 00000000..d800886d --- /dev/null +++ b/test/fixtures/files/2017/04/test.mp4 @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/test/fixtures/files/2017/04/test.odt b/test/fixtures/files/2017/04/test.odt new file mode 100644 index 00000000..5a96f2da Binary files /dev/null and b/test/fixtures/files/2017/04/test.odt differ diff --git a/test/fixtures/files/2017/04/test.pdf b/test/fixtures/files/2017/04/test.pdf new file mode 100644 index 00000000..d800886d --- /dev/null +++ b/test/fixtures/files/2017/04/test.pdf @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/test/fixtures/files/2017/04/test.txt b/test/fixtures/files/2017/04/test.txt new file mode 100644 index 00000000..d800886d --- /dev/null +++ b/test/fixtures/files/2017/04/test.txt @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/test/fixtures/files/2017/04/test2.txt b/test/fixtures/files/2017/04/test2.txt new file mode 100644 index 00000000..d800886d --- /dev/null +++ b/test/fixtures/files/2017/04/test2.txt @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/test/fixtures/files/2017/04/test4.txt b/test/fixtures/files/2017/04/test4.txt new file mode 100644 index 00000000..d800886d --- /dev/null +++ b/test/fixtures/files/2017/04/test4.txt @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/test/fixtures/files/2017/04/test5.txt b/test/fixtures/files/2017/04/test5.txt new file mode 100644 index 00000000..d800886d --- /dev/null +++ b/test/fixtures/files/2017/04/test5.txt @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/test/fixtures/files/2017/04/zero.txt b/test/fixtures/files/2017/04/zero.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/queries.yml b/test/fixtures/queries.yml new file mode 100644 index 00000000..67c834d9 --- /dev/null +++ b/test/fixtures/queries.yml @@ -0,0 +1,15 @@ +--- +queries_401: + id: 401 + type: DmsfQuery + project_id: 1 + visibility: 2 + name: Test + filters: | + --- + title: + :values: + - 'test' + :operator: '~' + user_id: 2 + column_names: \ No newline at end of file diff --git a/test/functional/dmsf_context_menus_controller_test.rb b/test/functional/dmsf_context_menus_controller_test.rb new file mode 100644 index 00000000..78423d33 --- /dev/null +++ b/test/functional/dmsf_context_menus_controller_test.rb @@ -0,0 +1,490 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Context menu controller +class DmsfContextMenusControllerTest < RedmineDmsf::Test::TestCase + include Redmine::I18n + + def setup + super + @file_link2 = DmsfLink.find 2 + @file_link6 = DmsfLink.find 6 + @folder_link1 = DmsfLink.find 1 + @url_link5 = DmsfLink.find 5 + end + + def test_dmsf_file + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings notified_events: ['dmsf_legacy_notifications'] do + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => '1', 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-edit', text: l(:button_edit) + assert_select 'a.icon-lock', text: l(:button_lock) + assert_select 'a.icon-email-add', text: l(:label_notifications_on) + assert_select 'a.icon-del', text: l(:button_delete) + assert_select 'a.icon-download', text: l(:button_download) + assert_select 'a.icon-email', text: l(:field_mail) + assert_select 'a.icon-file', text: l(:button_edit_content) + end + end + end + + def test_dmsf_file_locked + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings notified_events: ['dmsf_legacy_notifications'] do + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => '1', 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + get '/projects/dmsf/context_menu', params: { id: @file2.project.id, ids: ["file-#{@file2.id}"] } + assert_response :success + assert_select 'a.icon-edit.disabled', text: l(:button_edit) + assert_select 'a.icon-unlock', text: l(:button_unlock) + assert_select 'a.icon-lock', text: l(:button_lock), count: 0 + assert_select 'a.icon-email-add.disabled', text: l(:label_notifications_on) + assert_select 'a.icon-del.disabled', text: l(:button_delete) + assert_select 'a.icon-file.disabled', text: l(:button_edit_content) + end + end + end + + def test_dmsf_edit_file_locked_by_myself + post '/login', params: { username: 'jsmith', password: 'jsmith' } + User.current = @jsmith + @file1.lock! + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => '1', 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + 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 + end + end + + def test_dmsf_file_locked_force_unlock_permission_off + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/context_menu', params: { id: @file2.project.id, ids: ["file-#{@file2.id}"] } + assert_response :success + assert_select 'a.icon-unlock.disabled', text: l(:button_unlock) + end + + def test_dmsf_file_locked_force_unlock_permission_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_developer.add_permission! :force_file_unlock + get '/projects/dmsf/context_menu', params: { id: @file2.project.id, ids: ["file-#{@file2.id}"] } + assert_response :success + assert_select 'a.icon-unlock.disabled', text: l(:button_unlock), count: 0 + end + + def test_dmsf_file_notification_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @file1.notify_activate + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-email', text: l(:label_notifications_off) + assert_select 'a.icon-email-add', text: l(:label_notifications_on), count: 0 + end + end + + def test_dmsf_file_manipulation_permission_off + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_manipulation + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-edit.disabled', text: l(:button_edit) + assert_select 'a.icon-lock.disabled', text: l(:button_lock) + assert_select 'a.icon-email-add.disabled', text: l(:label_notifications_on) + assert_select 'a.icon-del.disabled', text: l(:button_delete) + end + end + + def test_dmsf_file_manipulation_permission_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a:not(icon-edit.disabled)', text: l(:button_edit) + assert_select 'a:not(icon-lock.disabled)', text: l(:button_lock) + assert_select 'a:not(icon-email-add.disabled)', text: l(:label_notifications_on) + assert_select 'a:not(icon-del.disabled)', text: l(:button_delete) + end + end + + def test_dmsf_file_email_permission_off + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :email_documents + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-email.disabled', text: l(:field_mail) + end + + def test_dmsf_file_email_permission_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :email_document + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a:not(icon-email.disabled)', text: l(:field_mail) + end + + def test_dmsf_file_delete_permission_off + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_manipulation + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-del.disabled', text: l(:button_delete) + end + + def test_dmsf_file_delete_permission_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a:not(icon-del.disabled)', text: l(:button_delete) + assert_select 'a.icon-del', text: l(:button_delete) + end + + def test_dmsf_file_edit_content + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => '1', 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-file', text: l(:button_edit_content) + end + end + + def test_dmsf_file_edit_content_webdav_disabled + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => nil } do + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a:not(icon-file)' + end + end + + def test_dmsf_file_edit_content_webdav_readonly + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => '1', 'dmsf_webdav_strategy' => 'WEBDAV_READ_ONLY' } do + get '/projects/dmsf/context_menu', params: { id: @file1.project.id, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-file.disabled', text: l(:button_edit_content) + end + end + + def test_dmsf_file_watch + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/context_menu', params: { id: @file1.project, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-fav-off', text: l(:button_watch) + end + + def test_dmsf_file_unwatch + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @file1.add_watcher @jsmith + get '/projects/dmsf/context_menu', params: { id: @file1.project, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-fav', text: l(:button_unwatch) + end + + def test_dmsf_file_link + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings notified_events: ['dmsf_legacy_notifications'] do + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => '1', 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + get '/projects/dmsf/context_menu', + params: { id: @file_link6.project.id, folder_id: @file_link6.dmsf_folder, + ids: ["file-link-#{@file_link6.id}"] } + assert_response :success + assert_select 'a.icon-edit', text: l(:button_edit) + assert_select 'a.icon-lock', text: l(:button_lock) + assert_select 'a.icon-email-add', text: l(:label_notifications_on) + assert_select 'a.icon-del', text: l(:button_delete) + assert_select 'a.icon-download', text: l(:button_download) + assert_select 'a.icon-email', text: l(:field_mail) + assert_select 'a.icon-file', text: l(:button_edit_content) + end + end + end + + def test_dmsf_file_link_locked + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert @file_link2.target_file.locked? + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', + params: { id: @file_link2.project.id, folder_id: @file_link2.dmsf_folder.id, + ids: ["file-link-#{@file_link2.id}"] } + assert_response :success + assert_select 'a.icon-edit.disabled', text: l(:button_edit) + assert_select 'a.icon-unlock', text: l(:button_unlock) + assert_select 'a.icon-lock', text: l(:button_lock), count: 0 + assert_select 'a.icon-email-add.disabled', text: l(:label_notifications_on) + assert_select 'a.icon-del', text: l(:button_delete) + end + end + + def test_dmsf_url_link + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/context_menu', params: { id: @url_link5.project.id, ids: ["url-link-#{@url_link5.id}"] } + assert_response :success + assert_select 'a.icon-del', text: l(:button_delete) + end + + def test_dmsf_folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @folder1.project.id, ids: ["folder-#{@folder1.id}"] } + assert_response :success + assert_select 'a.icon-edit', text: l(:button_edit) + assert_select 'a.icon-lock', text: l(:button_lock) + assert_select 'a.icon-email-add', text: l(:label_notifications_on) + assert_select 'a.icon-del', text: l(:button_delete) + assert_select 'a:not(icon-del.disabled)', text: l(:button_delete) + assert_select 'a.icon-download', text: l(:button_download) + assert_select 'a.icon-email', text: l(:field_mail) + end + end + + def test_dmsf_folder_locked + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert @folder5.locked? + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @folder5.project.id, ids: ["folder-#{@folder5.id}"] } + assert_response :success + assert_select 'a.icon-edit.disabled', text: l(:button_edit) + assert_select 'a.icon-unlock', text: l(:button_unlock) + assert_select 'a.icon-lock', text: l(:button_lock), count: 0 + assert_select 'a.icon-email-add.disabled', text: l(:label_notifications_on) + assert_select 'a.icon-del.disabled', text: l(:button_delete) + end + end + + def test_dmsf_folder_locked_force_unlock_permission_off + post '/login', params: { username: 'dlopper', password: 'foo' } + get '/projects/dmsf/context_menu', params: { id: @folder2.project.id, ids: ["folder-#{@folder2.id}"] } + assert_response :success + # @folder2 is locked by @jsmith, therefore @dlopper can't unlock it + assert_select 'a.icon-unlock.disabled', text: l(:button_unlock) + end + + def test_dmsf_folder_locked_force_unlock_permission_om + post '/login', params: { username: 'dlopper', password: 'foo' } + @role_developer.add_permission! :force_file_unlock + get '/projects/dmsf/context_menu', params: { id: @folder2.project.id, ids: ["folder-#{@folder2.id}"] } + assert_response :success + # @folder2 is locked by @jsmith, but @dlopper can unlock it + assert_select 'a.icon-unlock.disabled', text: l(:button_unlock), count: 0 + end + + def test_dmsf_folder_notification_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @folder5.notify_activate + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @folder5.project.id, ids: ["folder-#{@folder5.id}"] } + assert_response :success + assert_select 'a.icon-email', text: l(:label_notifications_off) + assert_select 'a.icon-email-add', text: l(:label_notifications_on), count: 0 + end + end + + def test_dmsf_folder_manipulation_permmissions_off + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :folder_manipulation + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @folder1.project.id, ids: ["folder-#{@folder1.id}"] } + assert_response :success + assert_select 'a.icon-edit.disabled', text: l(:button_edit) + assert_select 'a.icon-lock.disabled', text: l(:button_lock) + assert_select 'a.icon-email-add.disabled', text: l(:label_notifications_on) + assert_select 'a.icon-del.disabled', text: l(:button_delete) + end + end + + def test_dmsf_folder_manipulation_permmissions_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @folder1.project.id, ids: ["folder-#{@folder1.id}"] } + assert_response :success + assert_select 'a:not(icon-edit.disabled)', text: l(:button_edit) + assert_select 'a:not(icon-lock.disabled)', text: l(:button_lock) + assert_select 'a:not(icon-email-add.disabled)', text: l(:label_notifications_on) + assert_select 'a:not(icon-del.disabled)', text: l(:button_delete) + end + end + + def test_dmsf_folder_email_permmissions_off + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :email_documents + get '/projects/dmsf/context_menu', params: { id: @folder5.project.id, ids: ["folder-#{@folder5.id}"] } + assert_response :success + assert_select 'a.icon-email.disabled', text: l(:field_mail) + end + + def test_dmsf_folder_email_permmissions_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/context_menu', params: { id: @folder5.project.id, ids: ["folder-#{@folder5.id}"] } + assert_response :success + assert_select 'a:not(icon-email.disabled)', text: l(:field_mail) + end + + def test_dmsf_folder_watch + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/context_menu', params: { id: @folder1.project, ids: ["folder-#{@folder1.id}"] } + assert_response :success + assert_select 'a.icon-fav-off', text: l(:button_watch) + end + + def test_dmsf_folder_unwatch + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @folder1.add_watcher @jsmith + get '/projects/dmsf/context_menu', params: { id: @folder1.project, ids: ["folder-#{@folder1.id}"] } + assert_response :success + assert_select 'a.icon-fav', text: l(:button_unwatch) + end + + def test_dmsf_folder_link + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @folder_link1.project.id, ids: ["folder-#{@folder_link1.id}"] } + assert_response :success + assert_select 'a.icon-edit', text: l(:button_edit) + assert_select 'a.icon-lock', text: l(:button_lock) + assert_select 'a.icon-email-add', text: l(:label_notifications_on) + assert_select 'a.icon-del', text: l(:button_delete) + assert_select 'a.icon-download', text: l(:button_download) + assert_select 'a.icon-email', text: l(:field_mail) + end + end + + def test_dmsf_folder_link_locked + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @folder_link1.target_folder.lock! + with_settings notified_events: ['dmsf_legacy_notifications'] do + get '/projects/dmsf/context_menu', params: { id: @folder_link1.project.id, ids: ["folder-#{@folder_link1.id}"] } + assert_response :success + assert_select 'a.icon-edit.disabled', text: l(:button_edit) + assert_select 'a.icon-unlock', text: l(:button_unlock) + assert_select 'a.icon-lock', text: l(:button_lock), count: 0 + assert_select 'a.icon-email-add.disabled', text: l(:label_notifications_on) + assert_select 'a.icon-del.disabled', text: l(:button_delete) + end + end + + def test_dmsf_multiple + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/context_menu', params: { id: @project1.id, ids: ["folder-#{@folder1.id}", "file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-edit', text: l(:button_edit), count: 0 + assert_select 'a.icon-unlock', text: l(:button_unlock), count: 0 + assert_select 'a.icon-lock', text: l(:button_lock), count: 0 + assert_select 'a.icon-email-add', text: l(:label_notifications_on), count: 0 + assert_select 'a.icon-email', text: l(:label_notifications_off), count: 0 + assert_select 'a.icon-del', text: l(:button_delete) + assert_select 'a.icon-download', text: l(:button_download) + assert_select 'a.icon-email', text: l(:field_mail) + end + + def test_dmsf_project_watch + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/context_menu', params: { ids: ["project-#{@project1.id}"] } + assert_response :success + assert_select 'a.icon-fav-off', text: l(:button_watch) + end + + def test_dmsf_project_unwatch + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @project1.add_watcher @jsmith + get '/projects/dmsf/context_menu', params: { ids: ["project-#{@project1.id}"] } + assert_response :success + assert_select 'a.icon-fav', text: l(:button_unwatch) + end + + def test_trash_file + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@file1.project.id}/dmsf/trash/context_menu", params: { ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-cancel', text: l(:title_restore) + assert_select 'a.icon-del', text: l(:button_delete) + end + + def test_trash_file_manipulation_permissions_off + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_delete + @role_manager.remove_permission! :file_manipulation + get "/projects/#{@file1.project.id}/dmsf/trash/context_menu", params: { ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-cancel.disabled', text: l(:title_restore) + assert_select 'a.icon-del.disabled', text: l(:button_delete) + end + + def test_trash_file_manipulation_permissions_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@file1.project.id}/dmsf/trash/context_menu", params: { ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a:not(icon-cancel.disabled)', text: l(:title_restore) + assert_select 'a:not(icon-del.disabled)', text: l(:button_delete) + end + + def test_trash_file_delete_permissions_off + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_delete + get "/projects/#{@file1.project.id}/dmsf/trash/context_menu", params: { ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a.icon-del.disabled', text: l(:button_delete) + end + + def test_trash_file_delete_permissions_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@file1.project.id}/dmsf/trash/context_menu", params: { ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'a:not(icon-del.disabled)', text: l(:button_delete) + end + + def test_trash_folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@folder5.project.id}/dmsf/trash/context_menu", params: { ids: ["folder-#{@folder5.id}"] } + assert_response :success + assert_select 'a.icon-cancel', text: l(:title_restore) + assert_select 'a.icon-del', text: l(:button_delete) + end + + def test_trash_folder_manipulation_permissions_off + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :folder_manipulation + get "/projects/#{@folder1.project.id}/dmsf/trash/context_menu", params: { ids: ["folder-#{@folder1.id}"] } + assert_response :success + assert_select 'a.icon-cancel.disabled', text: l(:title_restore) + assert_select 'a.icon-del.disabled', text: l(:button_delete) + end + + def test_trash_folder_manipulation_permissions_on + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@folder1.project.id}/dmsf/trash/context_menu", params: { ids: ["folder-#{@folder1.id}"] } + assert_response :success + assert_select 'a:not(icon-cancel.disabled)', text: l(:title_restore) + assert_select 'a:not(icon-del.disabled)', text: l(:button_delete) + end + + def test_trash_multiple + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@file1.project.id}/dmsf/trash/context_menu", + params: { ids: ["file-#{@file1.id}", "folder-#{@folder1.id}"] } + assert_response :success + assert_select 'a.icon-cancel', text: l(:title_restore) + assert_select 'a.icon-del', text: l(:button_delete) + end +end diff --git a/test/functional/dmsf_controller_test.rb b/test/functional/dmsf_controller_test.rb new file mode 100644 index 00000000..372c5279 --- /dev/null +++ b/test/functional/dmsf_controller_test.rb @@ -0,0 +1,713 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) +require File.expand_path('../../../lib/redmine_dmsf/dmsf_zip', __FILE__) + +# DMSF controller +class DmsfControllerTest < RedmineDmsf::Test::TestCase + include Redmine::I18n + include DmsfHelper + + def setup + super + @link2 = DmsfLink.find 2 + @link1 = DmsfLink.find 1 + @link4 = DmsfLink.find 4 + @custom_field = CustomField.find 21 + @custom_value = CustomValue.find 21 + default_url_options[:host] = 'www.example.com' + end + + def test_edit_folder_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :folder_manipulation + get "/projects/#{@project1.id}/dmsf/edit", params: { folder_id: @folder1 } + assert_response :forbidden + end + + def test_edit_folder_allowed + # Permissions OK + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/dmsf/edit", params: { folder_id: @folder1.id } + assert_response :success + # Custom fields + assert_select 'label', { text: @custom_field.name } + assert_select 'option', { value: @custom_value.value } + # Permissions - The form must contain a checkbox for each available role + roles = @project1.members.map(&:roles).flatten.uniq + roles.each do |r| + assert_select 'input', { value: r.name } + end + end + + def test_edit_folder_redirection_to_the_parent_folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/projects/#{@project1.id}/dmsf/save", + params: { folder_id: @folder2.id, parent_id: @folder2.dmsf_folder.id, + dmsf_folder: { title: @folder2.title, description: 'Updated folder' } } + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder2.dmsf_folder.id) + end + + def test_edit_folder_redirection_to_the_same_folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/projects/#{@project1.id}/dmsf/save", + params: { folder_id: @folder2.id, parent_id: @folder2.dmsf_folder.id, + dmsf_folder: { title: @folder2.title, description: 'Updated folder', + redirect_to_folder_id: @folder2.id } } + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder2.id) + end + + def test_trash_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_delete + get "/projects/#{@project1.id}/dmsf/trash" + assert_response :forbidden + end + + def test_trash_allowed + # Permissions OK + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/dmsf/trash" + assert_response :success + assert_select 'h2', { text: l(:link_trash_bin) } + end + + def test_trash + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @folder1.delete commit: false + get "/projects/#{@project1.id}/dmsf/trash" + assert_response :success + assert_select 'a', href: dmsf_folder_path(id: @folder1.project, folder_id: @folder1.id) + assert_select 'a', href: dmsf_folder_path(id: @folder2.project, folder_id: @folder2.id) + assert_select 'a', href: url_for(controller: :dmsf_files, action: 'view', id: @file4.id, only_path: true) + assert_select 'a', href: url_for(controller: :dmsf_files, action: 'view', id: @link2.target_id, only_path: true) + end + + def test_empty_trash + post '/login', params: { username: 'jsmith', password: 'jsmith' } + delete "/projects/#{@project1.id}/dmsf/empty_trash" + assert_equal 0, DmsfFolder.deleted.where(project_id: @project1.id).all.size + assert_equal 0, DmsfFile.deleted.where(project_id: @project1.id).all.size + assert_equal 0, DmsfLink.deleted.where(project_id: @project1.id).all.size + assert_redirected_to trash_dmsf_path(id: @project1) + end + + def test_empty_trash_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_delete + delete "/projects/#{@project1.id}/dmsf/empty_trash" + assert_response :forbidden + end + + def test_delete_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :folder_manipulation + delete "/projects/#{@project1.id}/dmsf/delete", params: { folder_id: @folder1.id, commit: false } + assert_response :forbidden + end + + def test_delete_locked + # Permissions OK but the folder is locked + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @request.env['HTTP_REFERER'] = dmsf_folder_path(id: @project1, folder_id: @folder2.id) + delete "/projects/#{@project1.id}/dmsf/delete", params: { folder_id: @folder2.id, commit: false } + assert_response :redirect + assert_include l(:error_folder_is_locked), flash[:error] + end + + def test_delete_ok + # Empty and not locked folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @request.env['HTTP_REFERER'] = dmsf_folder_path(id: @project1) + delete "/projects/#{@project1.id}/dmsf/delete", + params: { folder_id: @folder1.id, commit: false } + assert_redirected_to dmsf_folder_path(id: @project1) + end + + def test_delete_subfolder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @request.env['HTTP_REFERER'] = dmsf_folder_path(id: @project1, folder_id: @folder1.id) + delete "/projects/#{@folder2.project.id}/dmsf/delete", + params: { folder_id: @folder2.id, parent_id: @folder1.id, commit: false } + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder1.id) + end + + def test_restore_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_developer.remove_permission! :folder_manipulation + @folder4.deleted = 1 + @folder4.save + get "/projects/#{@folder4.project.id}/dmsf/restore", params: { folder_id: @folder4.id } + assert_response :forbidden + end + + def test_restore_ok + # Permissions OK + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @request.env['HTTP_REFERER'] = trash_dmsf_path(id: @project1) + @folder1.deleted = 1 + @folder1.save + get "/projects/#{@project1.id}/dmsf/restore", params: { folder_id: @folder1.id } + assert_response :redirect + end + + def test_delete_entries_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :folder_manipulation + post "/projects/#{@project1.id}/dmsf/entries", + params: { delete_entries: true, + ids: ["folder-#{@folder1.id}", "file-#{@file1.id}", "folder-link-#{@link1.id}", + "file-link-#{@link2.id}"] } + assert_response :forbidden + end + + def test_delete_entries_ok + # Permissions OK + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @request.env['HTTP_REFERER'] = dmsf_folder_path(id: @project1) + flash[:error] = nil + post "/projects/#{@project1.id}/dmsf/entries", + params: { delete_entries: true, ids: ["folder-#{@folder7.id}", "file-#{@file1.id}", "file-link-#{@link2.id}"] } + assert_response :redirect + assert_nil flash[:error] + end + + def test_restore_entries + # Restore + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @request.env['HTTP_REFERER'] = trash_dmsf_path(id: @project1) + flash[:error] = nil + post "/projects/#{@project1.id}/dmsf/entries", + params: { restore_entries: true, ids: ["file-#{@file1.id}", "file-link-#{@link2.id}"] } + assert_response :redirect + assert_nil flash[:error] + end + + def test_show + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => '1', 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + get "/projects/#{@project1.id}/dmsf" + assert_response :success + # New file link + assert_select 'a[href$=?]', '/dmsf/upload/multi_upload' + # New folder link + assert_select 'a[href$=?]', '/dmsf/new' + # Filters + assert_select 'fieldset#filters' + # Options + assert_select 'fieldset#options' + # Options - no "Group by" + assert_select 'select#group_by', count: 0 + # The main table + assert_select 'table.dmsf' + # CSV export + assert_select 'a.csv' + # WebDAV + assert_select 'a.webdav', text: 'WebDAV' + # 'Zero Size File' document and an expander is present + assert_select 'a', text: @file10.title + assert_select 'span.dmsf-expander' + end + end + + def test_show_webdav_disabled + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => nil } do + get "/projects/#{@project1.id}/dmsf" + assert_response :success + assert_select 'a.webdav', text: 'WebDAV', count: 0 + end + end + + def test_show_filters_found + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/dmsf", params: { f: ['title'], op: { 'title' => '~' }, v: { 'title' => ['Zero'] } } + assert_response :success + # 'Zero Size File' document + assert_select 'a', text: @file10.title + # No expander if a filter is set + assert_select 'span.dmsf-expander', count: 0 + end + + def test_show_filters_not_found + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/dmsf", params: { f: ['title'], op: { 'title' => '~' }, v: { 'title' => ['xxx'] } } + assert_response :success + # 'Zero Size File' document + assert_select 'a', text: @file10.title, count: 0 + end + + def test_show_filters_custom_field + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/dmsf", + params: { set_filter: '1', f: ['cf_21', ''], op: { 'cf_21' => '=' }, v: { 'cf_21' => ['User documentation'] } } + assert_response :success + # Folder 1 with Tag=User documentation + assert_select 'a', text: @folder1.title + # Other document/folders are not present + assert_select 'a', text: @file10.title, count: 0 + end + + def test_show_without_file_manipulation + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_manipulation + get "/projects/#{@project1.id}/dmsf" + assert_response :success + # New file link should be missing + assert_select 'a[href$=?]', '/dmsf/upload/multi_upload', count: 0 + end + + def test_show_csv + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/dmsf", params: { format: 'csv' } + assert_response :success + assert @response.media_type.include?('text/csv') + end + + def test_show_locked + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project2.id}/dmsf" + assert_response :success + # An unlock icon next to a locked file + assert_select 'a.icon-unlock', count: 1 + end + + def test_show_folder_doesnt_correspond_the_project + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert @project1 != @folder3.project + get "/projects/#{@project1.id}/dmsf", params: { folder_id: @folder3.id } + assert_response :not_found + end + + def test_folder_link_to_folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@link1.project_id}/dmsf", params: { folder_id: @link1.dmsf_folder_id } + assert_response :success + assert_select 'a', text: @link1.title, count: 1 + assert_select 'a[href$=?]', + "/projects/#{@link1.target_project.identifier}/dmsf?folder_id=#{@link1.target_folder.id}", + count: 4 # Two because of folder1 and folder1_link (2x - icon + link) + end + + def test_folder_link_to_project + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @link1.target_project_id = @project2.id + @link1.target_id = nil + assert @link1.save + get "/projects/#{@link1.project_id}/dmsf", params: { folder_id: @link1.dmsf_folder_id } + assert_response :success + assert_select 'a', text: @link1.title, count: 1 + assert_select 'a[href$=?]', "/projects/#{@project2.identifier}/dmsf", count: 2 # (2x - icon + link) + end + + def test_new_forbidden + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :folder_manipulation + get "/projects/#{@project1.id}/dmsf/new" + assert_response :forbidden + end + + def test_new + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/dmsf/new" + assert_response :success + end + + def test_email_entries_email_from_forbidden + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :email_documents + with_settings plugin_redmine_dmsf: { 'dmsf_documents_email_from' => 'karel.picman@kontron.com' } do + post "/projects/#{@project1.id}/dmsf/entries", params: { email_entries: true, ids: ["file-#{@file1.id}"] } + assert_response :forbidden + end + end + + def test_email_entries_email_from + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings plugin_redmine_dmsf: { 'dmsf_documents_email_from' => 'karel.picman@kontron.com' } do + post "/projects/#{@project1.id}/dmsf/entries", params: { email_entries: true, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select "input:match('value', ?)", RedmineDmsf.dmsf_documents_email_from + end + end + + def test_email_entries_reply_to + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings plugin_redmine_dmsf: { 'dmsf_documents_email_reply_to' => 'karel.picman@kontron.com' } do + post "/projects/#{@project1.id}/dmsf/entries", params: { email_entries: true, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select "input:match('value', ?)", RedmineDmsf.dmsf_documents_email_reply_to + end + end + + def test_email_entries_links_only + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings plugin_redmine_dmsf: { 'dmsf_documents_email_links_only' => '1' } do + post "/projects/#{@project1.id}/dmsf/entries", params: { email_entries: true, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_select 'input[id=email_links_only][value=1]' + end + end + + def test_entries_email + post '/login', params: { username: 'jsmith', password: 'jsmith' } + zip_file = Tempfile.new('test', Rails.root.join('tmp')) + post "/projects/#{@project1.id}/dmsf/entries/email", + params: { email: { to: 'to@test.com', from: 'from@test.com', subject: 'subject', body: 'body', + expired_at: '2015-01-01', folders: [], files: [@file1.id], + zipped_content: zip_file.path } } + assert_redirected_to dmsf_folder_path(id: @project1) + ensure + zip_file.unlink + end + + def test_download_email_entries + post '/login', params: { username: 'jsmith', password: 'jsmith' } + zip_file = Tempfile.new([RedmineDmsf::DmsfZip::FILE_PREFIX, '.zip'], Rails.root.join('tmp')) + entry = tmp_entry_identifier(zip_file.path) + get "/projects/#{@project1.identifier}/dmsf/entries/#{entry}/download_email_entries" + assert_response :success + end + + def test_download_email_entries_not_found + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.identifier}/dmsf/entries/notfound/download_email_entries" + assert_response :not_found + end + + def test_download_email_entries_forbidden + @role_manager.remove_permission! :view_dmsf_files + post '/login', params: { username: 'jsmith', password: 'jsmith' } + zip_file = Tempfile.new([RedmineDmsf::DmsfZip::FILE_PREFIX, '.zip'], Rails.root.join('tmp')) + entry = tmp_entry_identifier(zip_file.path) + get "/projects/#{@project1.identifier}/dmsf/entries/#{entry}/download_email_entries" + assert_response :forbidden + end + + def test_add_email_forbidden + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :view_dmsf_files + get '/projects/dmsf/add_email', params: { id: @project1.id }, xhr: true + assert_response :forbidden + end + + def test_add_email + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/add_email', params: { id: @project1.id }, xhr: true + assert_response :success + end + + def test_append_email_forbidden + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :view_dmsf_files + post '/projects/dmsf/append_email', + params: { id: @project1.id, user_ids: @project1.members.collect { |m| m.user.id }, format: 'js' } + assert_response :forbidden + end + + def test_append_email + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post '/projects/dmsf/append_email', + params: { id: @project1.id, user_ids: @project1.members.collect { |m| m.user.id }, format: 'js' } + assert_response :success + end + + def test_autocomplete_for_user_forbidden + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :view_dmsf_files + get '/projects/dmsf/autocomplete_for_user', params: { id: @project1.id }, xhr: true + assert_response :forbidden + end + + def test_autocomplete_for_user + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/projects/dmsf/autocomplete_for_user', params: { id: @project1.id }, xhr: true + assert_response :success + end + + def test_create_folder_in_root + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfFolder.count', +1 do + post "/projects/#{@project1.id}/dmsf/create", + params: { dmsf_folder: { title: 'New folder', description: 'Unit tests' } } + end + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: nil) + end + + def test_create_folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfFolder.count', +1 do + post "/projects/#{@project1.id}/dmsf/create", + params: { parent_id: @folder1.id, dmsf_folder: { title: 'New folder', description: 'Unit tests' } } + end + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder1) + end + + def test_show_with_sub_projects + post '/login', params: { username: 'jsmith', password: 'jsmith' } + with_settings plugin_redmine_dmsf: { 'dmsf_projects_as_subfolders' => '1' } do + get "/projects/#{@project1.id}/dmsf" + assert_response :success + # @project5 is as a sub-folder + assert_select "tr##{@project5.id}pspan", count: 1 + end + end + + def test_show_without_sub_projects + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/dmsf" + assert_response :success + # @project5 is not as a sub-folder + assert_select "tr##{@project5.id}pspan", count: 0 + end + + def test_show_default_sort_column + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/dmsf" + assert_response :success + # The default column Title's header is displayed as sorted '^' + assert_select 'a.icon-sorted-desc', text: l(:label_column_title) + end + + def test_index + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf' + assert_response :success + # Projects + assert_select 'table.dmsf' do + assert_select 'tr' do + assert_select 'td.dmsf-title' do + assert_select 'a', text: "[#{@project1.name}]" + assert_select 'a', text: "[#{@project2.name}]" + end + end + end + # No context menu + assert_select 'div.contextual', count: 0 + # No description + assert_select 'div.dmsf-header', count: 0 + # No CSV export + assert_select 'a.csv', count: 0 + end + + def test_index_non_member + post '/login', params: { username: 'dlopper', password: 'foo' } + get '/dmsf' + assert_response :success + assert_select 'table.dmsf' do + assert_select 'tr' do + assert_select 'td.dmsf-title' do + assert_select 'a', text: "[#{@project1.name}]" + assert_select 'a', text: "[#{@project2.name}]", count: 0 + end + end + end + end + + def test_index_no_membership + post '/login', params: { username: 'someone', password: 'foo' } + get '/dmsf' + assert_response :forbidden + end + + def test_copymove_authorize_admin + post '/login', params: { username: 'admin', password: 'admin' } + get "/projects/#{@project1.id}/entries/copymove", + params: { folder_id: @file1.dmsf_folder, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_template 'copymove' + end + + def test_copymove_authorize_non_member + post '/login', params: { username: 'someone', password: 'foo' } + get "/projects/#{@project1.id}/entries/copymove", + params: { folder_id: @file1.dmsf_folder, ids: ["file-#{@file1.id}"] } + assert_response :forbidden + end + + def test_copymove_authorize_member_no_module + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @file1.project.disable_module! :dmsf + get "/projects/#{@project1.id}/entries/copymove", + params: { folder_id: @file1.dmsf_folder, ids: ["file-#{@file1.id}"] } + assert_response :forbidden + end + + def test_copymove_authorize_forbidden + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :folder_manipulation + get "/projects/#{@project1.id}/entries/copymove", + params: { folder_id: @file1.dmsf_folder, ids: ["file-#{@file1.id}"] } + assert_response :forbidden + end + + def test_copymove_target_folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/projects/#{@project1.id}/entries/copymove", + params: { folder_id: @file1.dmsf_folder, ids: ["file-#{@file1.id}"] } + assert_response :success + assert_template 'copymove' + end + + def test_entries_copy + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/projects/#{@project1.id}/dmsf/entries", + params: { dmsf_entries: { target_project_id: @folder1.project.id, target_folder_id: @folder1.id }, + ids: ["file-#{@file1.id}"], + copy_entries: true } + assert_response :redirect + assert_nil flash[:error] + end + + def test_entries_copy_to_the_same_folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/projects/#{@project1.id}/dmsf/entries", + params: { dmsf_entries: { target_project_id: @file1.project.id, target_folder_id: @file1.dmsf_folder }, + ids: ["file-#{@file1.id}"], + copy_entries: true } + assert_response :redirect + assert_equal flash[:error], l(:error_target_folder_same) + end + + def test_entries_move_recursion + # Move a folder under the same folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/projects/#{@project1.id}/dmsf/entries", + params: { dmsf_entries: { target_project_id: @folder2.project.id, target_folder_id: @folder2.id }, + ids: ["folder-#{@folder1.id}"], + move_entries: true } + assert_response :redirect + assert_equal flash[:error], l(:error_target_folder_same) + end + + def test_entries_move_infinity + # Move the folder to itself + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/projects/#{@project1.id}/dmsf/entries", + params: { dmsf_entries: { target_project_id: @folder2.project.id, target_folder_id: @folder2.id }, + ids: ["folder-#{@folder2.id}"], + move_entries: true } + assert_response :redirect + assert_equal flash[:error], l(:error_target_folder_same) + end + + def test_entries_copy_to_locked_folder + post '/login', params: { username: 'admin', password: 'admin' } + post "/projects/#{@project1.id}/dmsf/entries", + params: { dmsf_entries: { target_project_id: @folder2.project.id, target_folder_id: @folder2.id }, + ids: ["file-#{@file1.id}"], + move_entries: true } + assert_response :forbidden + end + + def test_entries_copy_to_dmsf_not_enabled + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @project2.disable_module! :dmsf + post "/projects/#{@project2.id}/dmsf/entries", + params: { dmsf_entries: { target_project_id: @project2.id }, + ids: ["file-#{@file1.id}"], + copy_entries: true } + assert_response :forbidden + end + + def test_entries_copy_to_dmsf_enabled + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/projects/#{@project2.id}/dmsf/entries", + params: { dmsf_entries: { target_project_id: @project2.id }, + ids: ["file-#{@file1.id}"], + copy_entries: true } + assert_response :redirect + end + + def test_entries_copy_to_as_non_member + post '/login', params: { username: 'someone', password: 'foo' } + post "/projects/#{@project2.id}/dmsf/entries", + params: { dmsf_entries: { target_project_id: @project2.id }, + ids: ["file-#{@file1.id}"], + copy_entries: true } + assert_response :forbidden + end + + def test_copymove_new_fast_links_enabled + post '/login', params: { username: 'jsmith', password: 'jsmith' } + member = Member.find_by(user_id: @jsmith.id, project_id: @project1.id) + assert member + member.dmsf_fast_links = true + member.save + get "/projects/#{@project1.id}/entries/copymove", + params: { folder_id: @file1.dmsf_folder, ids: ["file-#{@file4.id}"] } + assert_response :success + assert_select 'label', { count: 0, text: l(:label_target_project) } + assert_select 'label', { count: 0, text: "#{l(:label_target_folder)}#" } + end + + def test_entries_move_fast_links_enabled + # Target project is not given + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/projects/#{@project1.id}/dmsf/entries", + params: { dmsf_entries: { target_folder_id: @folder1.id }, + ids: ["file-#{@file1.id}"], + move_entries: true } + assert_response :redirect + assert_nil flash[:error] + end + + def test_digest + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf/digest', xhr: true + assert_response :success + end + + def test_digest_unauthorized + get '/dmsf/digest', xhr: true + assert_response :unauthorized + end + + def test_reset_digest + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post '/dmsf/digest', params: { password: 'jsmith' } + assert_response :redirect + assert_redirected_to my_account_path + token = Token.find_by(user_id: @jsmith.id, action: 'dmsf_webdav_digest') + assert token + assert_equal ActiveSupport::Digest.hexdigest("jsmith:#{RedmineDmsf::Webdav::AUTHENTICATION_REALM}:jsmith"), + token.value + end + + def test_reset_digest_unauthorized + post '/dmsf/digest', params: { password: 'jsmith' } + assert_response :redirect + assert_redirected_to 'http://www.example.com/login?back_url=http%3A%2F%2Fwww.example.com%2Fdmsf%2Fdigest' + end + + def test_entries_download + # Target project is not given + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/projects/#{@project1.id}/dmsf/entries", + params: { ids: ["folder-link-#{@link1.id}", "file-link-#{@link4.id}"], download_entries: true } + assert_response :success + end +end diff --git a/test/functional/dmsf_files_controller_test.rb b/test/functional/dmsf_files_controller_test.rb new file mode 100644 index 00000000..e29fa353 --- /dev/null +++ b/test/functional/dmsf_files_controller_test.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Copyright © 2011-23 Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# DmsfFiles controller +class DmsfFilesControllerTest < RedmineDmsf::Test::TestCase + def teardown + super + DmsfFile.clear_previews + end + + def test_show_file_ok + # Permissions OK + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf/files/#{@file1.id}", params: { id: @file1.id } + assert_response :success + end + + def test_show_formatting_html + Setting.text_formatting = 'HTML' + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf/files/#{@file1.id}", params: { id: @file1.id } + assert_response :success + assert_include 'dmsf-description', response.body, 'dmsf-description class not found' + assert_include 'wiki-edit', response.body, 'wiki-edit class not found' + end + + def test_show_formatting_textile + Setting.text_formatting = 'Textile' + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf/files/#{@file1.id}", params: { id: @file1.id } + assert_response :success + assert_include 'dmsf-description', response.body, 'dmsf-description class not found' + assert_include 'wiki-edit', response.body, 'wiki-edit class not found' + end + + def test_show_file_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :view_dmsf_files + get "/dmsf/files/#{@file1.id}", params: { id: @file1.id } + assert_response :forbidden + end + + def test_view_file_standard_url + # Permissions OK + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf/files/#{@file1.id}/view", params: { id: @file1.id } + assert_response :success + end + + def test_view_file_pretty_url + # Permissions OK + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf/files/#{@file1.id}/test.txt", params: { id: @file1.id } + assert_response :success + end + + def test_view_file_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :view_dmsf_files + get "/dmsf/files/#{@file1.id}/view", params: { id: @file1.id } + assert_response :forbidden + end + + def test_view_preview + return unless RedmineDmsf::Preview.office_available? + + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf/files/#{@file13.id}/view", params: { id: @file13.id } + assert_response :success + assert_equal 'application/pdf', @response.media_type + assert @response.body.starts_with?('%PDF') + end + + def delete_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_manipulation + delete "/dmsf/files/#{@file1.id}", params: { id: @file1.id, folder_id: @file1.dmsf_folder.id, commit: false } + assert_response :forbidden + end + + def delete_locked + # Permissions OK but the file is locked + post '/login', params: { username: 'jsmith', password: 'jsmith' } + delete "/dmsf/files/#{@file2.id}", params: { id: @file2.id, folder_id: @file2.dmsf_folder.id, commit: false } + assert_response :redirect + assert_include l(:error_file_is_locked), flash[:error] + end + + def delete_ok + # Permissions OK and not locked + post '/login', params: { username: 'jsmith', password: 'jsmith' } + delete "/dmsf/files/#{@file1.id}", params: { id: @file1.id, folder_id: @file1.dmsf_folder.id, commit: false } + assert_redirected_to dmsf_folder_path(id: @file1.project, folder_id: @file1.dmsf_folder.id) + end + + def test_delete_in_subfolder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + delete "/dmsf/files/#{@file4.id}", params: { id: @file4.id, folder_id: @file4.dmsf_folder.id, commit: false } + assert_redirected_to dmsf_folder_path(id: @file4.project, folder_id: @file4.dmsf_folder.id) + end + + def test_obsolete_revision_ok + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf/files/#{@file1.id}/revision/obsolete", params: { id: @file1.last_revision.id } + assert_redirected_to action: 'show', id: @file1 + end + + def test_obsolete_revision_missing_permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_manipulation + get "/dmsf/files/#{@file1.id}/revision/obsolete", params: { id: @file1.last_revision.id } + assert :forbiden + end + + def test_create_revision + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfFileRevision.count', +1 do + post "/dmsf/files/#{@file1.id}/revision/create", + params: { + id: @file1.id, + 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, + comment: 'New revision' + } + } + end + assert_redirected_to dmsf_folder_path(id: @file1.project) + assert_not_nil @file1.last_revision.digest + end +end diff --git a/test/functional/dmsf_folder_permissions_controller_test.rb b/test/functional/dmsf_folder_permissions_controller_test.rb new file mode 100644 index 00000000..745e7add --- /dev/null +++ b/test/functional/dmsf_folder_permissions_controller_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Folder permissions controller +class DmsfFolderPermissionsControllerTest < RedmineDmsf::Test::TestCase + def setup + super + post '/login', params: { username: 'jsmith', password: 'jsmith' } + end + + def test_new + get '/dmsf_folder_permissions/new', + params: { project_id: @project1.id, dmsf_folder_id: @folder7.id, format: 'js' }, + xhr: true + assert_response :success + assert_template 'new' + assert @response.media_type.include?('text/javascript') + end + + def test_autocomplete_for_user + get '/dmsf_folder_permissions/autocomplete_for_user', + params: { project_id: @project1.id, dmsf_folder_id: @folder7.id, q: 'smi', format: 'js' }, + xhr: true + assert_response :success + assert_include @jsmith.name, response.body + end + + def test_append + post '/dmsf_folder_permissions/append', + params: { project_id: @project1.id, dmsf_folder_id: @folder7.id, user_ids: [@jsmith.id], format: 'js' }, + xhr: true + assert_response :success + assert_template 'append' + assert @response.content_type.match?(%r{^text/javascript}) + end +end diff --git a/test/functional/dmsf_help_controller_test.rb b/test/functional/dmsf_help_controller_test.rb new file mode 100644 index 00000000..5b69eafd --- /dev/null +++ b/test/functional/dmsf_help_controller_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Help controller +class DmsfHelpControllerTest < RedmineDmsf::Test::TestCase + def test_wiki_syntax + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf/help/wiki_syntax' + assert_response :success + assert_select 'h1', text: 'DMS Syntax Reference' + end + + def test_wiki_syntax_cs + @jsmith.language = 'cs' + assert @jsmith.save + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf/help/wiki_syntax' + assert_response :success + assert_select 'h1', text: 'Referenční dokumentace syntaxe DMS' + end + + def test_dmsf_help + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf/help/dmsf_help' + assert_response :success + assert_select 'h1', text: "DMSF User's guide" + end +end diff --git a/test/functional/dmsf_links_controller_test.rb b/test/functional/dmsf_links_controller_test.rb new file mode 100644 index 00000000..f490ac2c --- /dev/null +++ b/test/functional/dmsf_links_controller_test.rb @@ -0,0 +1,352 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Links controller +class DmsfLinksControllerTest < RedmineDmsf::Test::TestCase + include Redmine::I18n + + def setup + super + @file_link = DmsfLink.find 1 + @url_link = DmsfLink.find 5 + end + + def test_authorize_admin + post '/login', params: { username: 'admin', password: 'admin' } + get '/dmsf_links/new', params: { project_id: @project1.id } + assert_response :success + assert_template 'new' + end + + def test_authorize_non_member + post '/login', params: { username: 'someone', password: 'foo' } + get '/dmsf_links/new', params: { project_id: @project2.id } + assert_response :forbidden + end + + def test_authorize_member_ok + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf_links/new', params: { project_id: @project1.id } + assert_response :success + end + + def test_authorize_member_no_module + # Without the module + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @project1.disable_module! :dmsf + get '/dmsf_links/new', params: { project_id: @project1.id } + assert_response :forbidden + end + + def test_authorize_forbidden + # Without permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_manipulation + get '/dmsf_links/new', params: { project_id: @project1.id } + assert_response :forbidden + end + + def test_new + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf_links/new', params: { project_id: @project1.id, type: 'link_to' } + assert_response :success + assert_select 'label', { text: l(:label_target_project) } + end + + def test_new_fast_links_enabled + post '/login', params: { username: 'jsmith', password: 'jsmith' } + member = Member.find_by(user_id: @jsmith.id, project_id: @project1.id) + assert member + member.dmsf_fast_links = true + member.save + get '/dmsf_links/new', params: { project_id: @project1.id, type: 'link_to' } + assert_response :success + assert_select 'label', { count: 0, text: l(:label_target_project) } + end + + def test_create_file_link_from_f1 + post '/login', params: { username: 'jsmith', password: 'jsmith' } + # 1. File link in a folder from another folder + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + target_project_id: @project2.id, + dmsf_folder_id: @folder1.id, + target_file_id: @file6.id, + target_folder_id: @folder3.id, + name: 'file_link', + type: 'link_from' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder1.id) + end + + def test_create_file_link_from_f2 + post '/login', params: { username: 'jsmith', password: 'jsmith' } + # 2. File link in a folder from another root folder + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + dmsf_folder_id: @folder1.id, + target_project_id: @project2.id, + target_file_id: @file2.id, + target_folder_id: 'Documents', + name: 'file_link', + type: 'link_from' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder1.id) + end + + def test_create_file_link_from_f3 + # 3. File link in a root folder from another folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + target_project_id: @project2.id, + target_file_id: @file6.id, + target_folder_id: @folder3.id, + name: 'file_link', + type: 'link_from' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1) + end + + def test_create_file_link_from_f4 + # 4. File link in a root folder from another root folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + target_project_id: @project2.id, + target_file_id: @file2.id, + name: 'file_link', + type: 'link_from' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1) + end + + def test_create_folder_link_from_d1 + # 1. Folder link in a folder from another folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + dmsf_folder_id: @folder1.id, + target_project_id: @project2.id, + target_folder_id: @folder3.id, + name: 'folder_link', + type: 'link_from' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder1.id) + end + + def test_create_folder_link_from_d2 + # 2. Folder link in a folder from another root folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + dmsf_folder_id: @folder1.id, + target_project_id: @project2.id, + name: 'folder_link', + type: 'link_from' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder1.id) + end + + def test_create_folder_link_from_d3 + # 3. Folder link in a root folder from another folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + target_project_id: @project2.id, + target_folder_id: @folder3.id, + name: 'folder_link', + type: 'link_from' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1) + end + + def test_create_folder_link_from_d4 + # 4. Folder link in a root folder from another root folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + target_project_id: @project2.id, + name: 'folder_link', + type: 'link_from' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1) + end + + def test_create_file_link_to_f1 + # 1. File link to a root folder from another folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + dmsf_file_id: @file1.id, + target_project_id: @project2.id, + target_folder_id: @folder3.id, + name: 'file_link', + type: 'link_to' + } } + end + assert_redirected_to dmsf_file_path(@file1.id) + end + + def test_create_file_link_to_f2 + # 2. File link to a folder from another folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project2.id, + dmsf_folder_id: @folder3.id, + target_project_id: @project1.id, + target_folder_id: @folder1.id, + dmsf_file_id: @file6.id, + name: 'file_link', + type: 'link_to' + } } + end + assert_redirected_to dmsf_file_path(@file6.id) + end + + def test_create_file_link_to_f3 + # 3. File link to a root folder from another root folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project2.id, + target_project_id: @project1.id, + dmsf_file_id: @file6.id, + name: 'file_link', + type: 'link_to' + } } + end + assert_redirected_to dmsf_file_path(@file6.id) + end + + def test_create_file_link_to_f4 + # 4. File link to a folder from another root folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project2.id, + dmsf_folder_id: @folder3.id, + target_project_id: @project1.id, + dmsf_file_id: @file6.id, + name: 'file_link', + type: 'link_to' + } } + end + assert_redirected_to dmsf_file_path(@file6.id) + end + + def test_create_external_link_from + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + target_project_id: @project1.id, + name: 'file_link', + external_link: 'true', + type: 'link_from' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1) + end + + def test_create_folder_link_to_f1 + # 1. Folder link to a root folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + dmsf_folder_id: @folder1.id, + target_project_id: @project2.id, + name: 'folder_link', + type: 'link_to' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder1.dmsf_folder_id) + end + + def test_create_folder_link_to_f2 + # 2. Folder link to a folder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.count', +1 do + post '/dmsf_links', params: { dmsf_link: { + project_id: @project1.id, + dmsf_folder_id: @folder1.id, + target_project_id: @project2.id, + target_folder_id: @folder3.id, + name: 'folder_link', + type: 'link_to' + } } + end + assert_redirected_to dmsf_folder_path(id: @project1, folder_id: @folder1.dmsf_folder_id) + end + + def test_destroy + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.visible.count', -1 do + delete "/dmsf_links/#{@file_link.id}", params: { project_id: @project1.id } + end + assert_redirected_to dmsf_folder_path(id: @file_link.project, folder_id: @file_link.dmsf_folder_id) + end + + def test_destroy_in_subfolder + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfLink.visible.count', -1 do + delete "/dmsf_links/#{@url_link.id}", + params: { project_id: @url_link.project_id, folder_id: @url_link.dmsf_folder_id } + end + assert_redirected_to dmsf_folder_path(id: @url_link.project, folder_id: @url_link.dmsf_folder_id) + end + + def test_restore_forbidden + # Missing permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @request.env['HTTP_REFERER'] = trash_dmsf_path(id: @project1) + @role_manager.remove_permission! :file_manipulation + get "/dmsf/links/#{@file_link.id}/restore", params: { project_id: @project1.id } + assert_response :forbidden + end + + def test_restore_ok + # Permissions OK + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @request.env['HTTP_REFERER'] = trash_dmsf_path(id: @project1) + get "/dmsf/links/#{@file_link.id}/restore", params: { project_id: @project1.id } + assert_response :redirect + end +end diff --git a/test/functional/dmsf_public_urls_controller_test.rb b/test/functional/dmsf_public_urls_controller_test.rb new file mode 100644 index 00000000..c1aab7ae --- /dev/null +++ b/test/functional/dmsf_public_urls_controller_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Public URL controller +class DmsfPublicUrlsControllerTest < RedmineDmsf::Test::TestCase + def test_show_valid_url + get '/dmsf_public_urls', params: { token: 'd8d33e21914a433b280fdc94450ee212' } + assert_response :success + end + + def test_show_url_width_invalid_token + get '/dmsf_public_urls', params: { token: 'f8d33e21914a433b280fdc94450ee212' } + assert_response :not_found + end + + def test_show_url_that_has_expired + get '/dmsf_public_urls', params: { token: 'e8d33e21914a433b280fdc94450ee212' } + assert_response :not_found + end +end diff --git a/test/functional/dmsf_state_controller_test.rb b/test/functional/dmsf_state_controller_test.rb new file mode 100644 index 00000000..19e9b426 --- /dev/null +++ b/test/functional/dmsf_state_controller_test.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# State controller +class DmsfStateControllerTest < RedmineDmsf::Test::TestCase + include Redmine::I18n + + def setup + super + @query401 = Query.find 401 + end + + def test_user_pref_save_member + with_settings notified_events: ['dmsf_legacy_notifications'] do + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.add_permission! :user_preferences + post "/projects/#{@project1.id}/dmsf/state", + params: { email_notify: 1, title_format: '%t_%v', fast_links: 1, act_as_attachable: 1, + default_dmsf_query: @query401.id } + assert_redirected_to settings_project_path(@project1, tab: 'dmsf') + assert_not_nil flash[:notice] + assert_equal flash[:notice], l(:notice_your_preferences_were_saved) + member = @project1.members.find_by(user_id: @jsmith.id) + assert member + assert_equal true, member.dmsf_mail_notification + assert_equal '%t_%v', member.dmsf_title_format + assert_equal true, member.dmsf_fast_links + @project1.reload + assert_equal 1, @project1.dmsf_act_as_attachable + assert_equal @query401.id, @project1.default_dmsf_query_id + end + end + + def test_user_pref_save_whithout_email_notification_settings + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.add_permission! :user_preferences + post "/projects/#{@project1.id}/dmsf/state", + params: { title_format: '%t_%v', fast_links: 1, act_as_attachable: 2, + default_dmsf_query: @query401.id } + assert_redirected_to settings_project_path(@project1, tab: 'dmsf') + assert_not_nil flash[:notice] + assert_equal flash[:notice], l(:notice_your_preferences_were_saved) + member = @project1.members.find_by(user_id: @jsmith.id) + assert member + assert_not member.dmsf_mail_notification + assert_equal '%t_%v', member.dmsf_title_format + assert_equal true, member.dmsf_fast_links + @project1.reload + assert_equal 1, @project1.dmsf_act_as_attachable + assert_equal @query401.id, @project1.default_dmsf_query_id + end + + def test_user_pref_save_member_forbidden + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :user_preferences + post "/projects/#{@project1.id}/dmsf/state", params: { email_notify: 1, title_format: '%t_%v' } + assert_response :forbidden + end + + def test_user_pref_save_none_member + # Non Member + post '/login', params: { username: 'someone', password: 'foo' } + post "/projects/#{@project1.id}/dmsf/state", params: { email_notify: 1, title_format: '%t_%v' } + assert_response :forbidden + end + + def test_user_pref_save_admin + # Admin - non member + post '/login', params: { username: 'admin', password: 'admin' } + post "/projects/#{@project1.id}/dmsf/state", params: { email_notify: 1, title_format: '%t_%v' } + assert_redirected_to settings_project_path(@project1, tab: 'dmsf') + assert_not_nil flash[:warning] + assert_equal flash[:warning], l(:user_is_not_project_member) + end +end diff --git a/test/functional/dmsf_workflow_controller_test.rb b/test/functional/dmsf_workflow_controller_test.rb new file mode 100644 index 00000000..d611981d --- /dev/null +++ b/test/functional/dmsf_workflow_controller_test.rb @@ -0,0 +1,454 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Workflows controller +class DmsfWorkflowsControllerTest < RedmineDmsf::Test::TestCase + include Redmine::I18n + + def setup + super + @wfs1 = DmsfWorkflowStep.find 1 # step 1 + @wfs2 = DmsfWorkflowStep.find 2 # step 2 + @wfs3 = DmsfWorkflowStep.find 3 # step 1 + @wfs4 = DmsfWorkflowStep.find 4 # step 2 + @wfs5 = DmsfWorkflowStep.find 5 # step 3 + @wf1 = DmsfWorkflow.find 1 + @wf3 = DmsfWorkflow.find 3 + @wfsa2 = DmsfWorkflowStepAssignment.find 2 + @revision1 = DmsfFileRevision.find 1 + @revision2 = DmsfFileRevision.find 2 + end + + def test_authorize_admin + # Admin + post '/login', params: { username: 'admin', password: 'admin' } + get '/dmsf_workflows' + assert_response :success + assert_template 'index' + end + + def test_authorize_member + # Non member + post '/login', params: { username: 'someone', password: 'foo' } + get '/dmsf_workflows', params: { project_id: @project1.id } + assert_response :forbidden + end + + def test_authorize_administration + # Administration + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf_workflows' + assert_response :forbidden + end + + def test_authorize_projects + # Project + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf_workflows', params: { project_id: @project1.id } + assert_response :success + assert_template 'index' + end + + def test_authorize_manage_workflows_forbidden + # Without permissions + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :manage_workflows + get '/dmsf_workflows', params: { project_id: @project1.id } + assert_response :forbidden + end + + def test_authorization_file_approval_ok + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.add_permission! :file_approval + @revision2.dmsf_workflow_id = @wf1.id + get "/dmsf_workflows/#{@revision2.dmsf_workflow_id}/start", params: { dmsf_file_revision_id: @revision2.id } + assert_response :redirect + end + + def test_authorization_file_approval_forbidden + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :file_approval + @revision2.dmsf_workflow_id = @wf1.id + get "/dmsf_workflows/#{@revision2.dmsf_workflow_id}/start", params: { dmsf_file_revision_id: @revision2.id } + assert_response :forbidden + end + + def test_authorization_no_module + # Without the module + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.add_permission! :file_manipulation + @project1.disable_module! :dmsf + get '/dmsf_workflows', params: { project_id: @project1.id } + assert_response :forbidden + end + + def test_index_administration + post '/login', params: { username: 'admin', password: 'admin' } + get '/dmsf_workflows' + assert_response :success + assert_template 'index' + end + + def test_index_project + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf_workflows', params: { project_id: @project1.id } + assert_response :success + assert_template 'index' + end + + def test_new + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get '/dmsf_workflows/new', params: { project_id: @project1.id } + assert_response :success + assert_template 'new' + end + + def test_lock + post '/login', params: { username: 'jsmith', password: 'jsmith' } + patch "/dmsf_workflows/#{@wf1.id}", params: { dmsf_workflow: { status: DmsfWorkflow::STATUS_LOCKED } } + @wf1.reload + assert @wf1.locked?, "#{@wf1.name} status is #{@wf1.status}" + end + + def test_unlock + post '/login', params: { username: 'admin', password: 'admin' } + patch "/dmsf_workflows/#{@wf3.id}", params: { dmsf_workflow: { status: DmsfWorkflow::STATUS_ACTIVE } } + @wf3.reload + assert @wf3.active?, "#{@wf3.name} status is #{@wf3.status}" + end + + def test_show + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf_workflows/#{@wf1.id}" + assert_response :success + assert_template 'show' + end + + def test_create + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfWorkflow.count', +1 do + post '/dmsf_workflows', params: { dmsf_workflow: { name: 'wf4', project_id: @project1.id } } + end + assert_redirected_to settings_project_path(@project1, tab: 'dmsf_workflow') + end + + def test_create_with_the_same_name + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfWorkflow.count', 0 do + post '/dmsf_workflows', params: { dmsf_workflow: { name: @wf1.name, project_id: @project1.id } } + end + assert_response :success + assert_select_error(/#{l('activerecord.errors.messages.taken')}$/) + end + + def test_update + post '/login', params: { username: 'jsmith', password: 'jsmith' } + patch "/dmsf_workflows/#{@wf1.id}", params: { dmsf_workflow: { name: 'wf1a' } } + @wf1.reload + assert_equal 'wf1a', @wf1.name + end + + def test_destroy + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfWorkflow.count', -1 do + delete "/dmsf_workflows/#{@wf1.id}" + end + assert_redirected_to settings_project_path(@project1, tab: 'dmsf_workflow') + assert_equal 0, DmsfWorkflowStep.where(dmsf_workflow_id: @wf1.id).all.size + end + + def test_add_step + post '/login', params: { username: 'jsmith', password: 'jsmith' } + assert_difference 'DmsfWorkflowStep.count', +1 do + post "/dmsf_workflows/#{@wf1.id}/edit", + params: { commit: l(:dmsf_or), step: 1, name: '1st step', user_ids: [@someone.id] } + end + assert_response :success + ws = DmsfWorkflowStep.order(id: :desc).first + assert_equal @wf1.id, ws&.dmsf_workflow_id + assert_equal 1, ws.step + assert_equal '1st step', ws.name + assert_equal @someone.id, ws.user_id + assert_equal DmsfWorkflowStep::OPERATOR_OR, ws.operator + end + + def test_remove_step + post '/login', params: { username: 'jsmith', password: 'jsmith' } + n = DmsfWorkflowStep.where(dmsf_workflow_id: @wf1.id, step: @wfs1.step).count + assert_difference 'DmsfWorkflowStep.count', -n do + delete "/dmsf_workflows/#{@wf1.id}/edit", params: { step: @wfs1.id } + end + assert_response :redirect + ws = DmsfWorkflowStep.where(dmsf_workflow_id: @wf1.id).order(id: :asc).first + assert_equal 1, ws&.step + end + + def test_reorder_steps_to_lower + post '/login', params: { username: 'jsmith', password: 'jsmith' } + put "/dmsf_workflows/#{@wf1.id}/edit", params: { step: 1, dmsf_workflow: { position: 2 } } + assert_response :success + @wfs1.reload + @wfs2.reload + @wfs3.reload + @wfs4.reload + @wfs5.reload + assert_equal 1, @wfs2.step + assert_equal 1, @wfs3.step + assert_equal 2, @wfs1.step + assert_equal 2, @wfs4.step + assert_equal 3, @wfs5.step + end + + def test_reorder_steps_to_lowest + post '/login', params: { username: 'jsmith', password: 'jsmith' } + put "/dmsf_workflows/#{@wf1.id}/edit", params: { step: 1, dmsf_workflow: { position: 3 } } + assert_response :success + @wfs1.reload + @wfs2.reload + @wfs3.reload + @wfs4.reload + @wfs5.reload + assert_equal 1, @wfs2.step + assert_equal 1, @wfs3.step + assert_equal 2, @wfs5.step + assert_equal 3, @wfs1.step + assert_equal 3, @wfs4.step + end + + def test_reorder_steps_to_higher + post '/login', params: { username: 'jsmith', password: 'jsmith' } + put "/dmsf_workflows/#{@wf1.id}/edit", params: { step: 3, dmsf_workflow: { position: 2 } } + assert_response :success + @wfs1.reload + @wfs2.reload + @wfs3.reload + @wfs4.reload + @wfs5.reload + assert_equal 1, @wfs1.step + assert_equal 1, @wfs4.step + assert_equal 2, @wfs5.step + assert_equal 3, @wfs2.step + assert_equal 3, @wfs3.step + end + + def test_reorder_steps_to_highest + post '/login', params: { username: 'jsmith', password: 'jsmith' } + put "/dmsf_workflows/#{@wf1.id}/edit", params: { step: 3, dmsf_workflow: { position: '1' } } + assert_response :success + @wfs1.reload + @wfs2.reload + @wfs3.reload + @wfs4.reload + @wfs5.reload + assert_equal 1, @wfs5.step + assert_equal 2, @wfs1.step + assert_equal 2, @wfs4.step + assert_equal 3, @wfs2.step + assert_equal 3, @wfs3.step + end + + def test_action_approve + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/dmsf_workflows/#{@wf1.id}/new_action", + params: { + commit: l(:button_submit), + dmsf_workflow_step_assignment_id: @wfsa2.id, + dmsf_file_revision_id: @revision1.id, + step_action: DmsfWorkflowStepAction::ACTION_APPROVE, + user_id: nil, + note: '' + } + assert_redirected_to dmsf_folder_path(id: @project1) + assert DmsfWorkflowStepAction.exists?(dmsf_workflow_step_assignment_id: @wfsa2.id, + action: DmsfWorkflowStepAction::ACTION_APPROVE) + end + + def test_action_reject + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/dmsf_workflows/#{@wf1.id}/new_action", + params: { + commit: l(:button_submit), + dmsf_workflow_step_assignment_id: @wfsa2.id, + dmsf_file_revision_id: @revision2.id, + step_action: DmsfWorkflowStepAction::ACTION_REJECT, + note: 'Rejected because...' + } + assert_response :redirect + assert DmsfWorkflowStepAction.exists?(dmsf_workflow_step_assignment_id: @wfsa2.id, + action: DmsfWorkflowStepAction::ACTION_REJECT) + end + + def test_action + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf_workflows/#{@wf1.id}/action", + xhr: true, + params: { + project_id: @project1.id, + dmsf_workflow_step_assignment_id: @wfsa2.id, + dmsf_file_revision_id: @revision2.id, + title: l(:title_waiting_for_approval) + } + assert_response :success + assert_match(/ajax-modal/, response.body) + assert_template 'action' + end + + def test_new_action_delegate + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/dmsf_workflows/#{@wf1.id}/new_action", + params: { + commit: l(:button_submit), + dmsf_workflow_step_assignment_id: @wfsa2.id, + dmsf_file_revision_id: @revision2.id, + step_action: @admin.id * 10, + note: 'Delegated because...' + } + assert_redirected_to dmsf_folder_path(id: @project1) + assert DmsfWorkflowStepAction.exists?(dmsf_workflow_step_assignment_id: @wfsa2.id, + action: DmsfWorkflowStepAction::ACTION_DELEGATE) + @wfsa2.reload + assert_equal @wfsa2.user_id, @admin.id + end + + def test_assign + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf_workflows/#{@project1.id}/assign", + xhr: true, + params: { + dmsf_file_revision_id: @revision1.id, + title: l(:label_dmsf_wokflow_action_assign) + } + assert_response :success + assert_match(/ajax-modal/, response.body) + assert_template 'assign' + end + + def test_start + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @revision2.dmsf_workflow_id = @wf1.id + get "/dmsf_workflows/#{@revision2.dmsf_workflow_id}/start", params: { dmsf_file_revision_id: @revision2.id } + assert_redirected_to dmsf_folder_path(id: @project1) + end + + def test_assignment + post '/login', params: { username: 'jsmith', password: 'jsmith' } + post "/dmsf_workflows/#{@project1.id}/assignment", + params: { + commit: l(:button_submit), + id: @wf1.id, + dmsf_workflow_id: @wf1.id, + dmsf_file_revision_id: @revision2.id, + action: 'assignment' + } + assert_response :redirect + end + + def test_update_step_name + post '/login', params: { username: 'jsmith', password: 'jsmith' } + put "/dmsf_workflows/#{@wf1.id}/update_step", params: { step: @wfs2.step, dmsf_workflow: { step_name: 'new_name' } } + assert_response :redirect + # All steps in the same step must be renamed + @wfs2.reload + assert_equal 'new_name', @wfs2.name + @wfs3.reload + assert_equal 'new_name', @wfs3.name + # But not in others + @wfs1.reload + assert_equal '1st step', @wfs1.name + end + + def test_update_step_operators + post '/login', params: { username: 'jsmith', password: 'jsmith' } + put "/dmsf_workflows/#{@wf1.id}/update_step", + params: { + step: '1', + operator_step: { @wfs1.id.to_s => DmsfWorkflowStep::OPERATOR_OR.to_s }, + assignee: { @wfs1.id.to_s => @wfs1.user_id.to_s } + } + assert_response :redirect + @wfs1.reload + assert_equal @wfs1.operator, DmsfWorkflowStep::OPERATOR_OR + end + + def test_update_step_assignee + post '/login', params: { username: 'jsmith', password: 'jsmith' } + put "/dmsf_workflows/#{@wf1.id}/update_step", + params: { + step: '1', + operator_step: { @wfs1.id.to_s => DmsfWorkflowStep::OPERATOR_OR.to_s }, + assignee: { @wfs1.id.to_s => @someone.id.to_s } + } + assert_response :redirect + @wfs1.reload + assert_equal @someone.id, @wfs1.user_id + end + + def test_delete_step + post '/login', params: { username: 'jsmith', password: 'jsmith' } + n = DmsfWorkflowStep.where(dmsf_workflow_id: @wf1.id, step: @wfs2.step).count + assert_difference 'DmsfWorkflowStep.count', -n do + delete "/dmsf_workflows/#{@wfs1.id}/edit", params: { step: @wfs2.id } + end + assert_response :redirect + end + + def test_log_non_member + post '/login', params: { username: 'someone', password: 'foo' } + get "/dmsf_workflows/#{@wf1.id}/log", + params: { project_id: @project1.id, dmsf_file_id: @file1.id, format: 'js' }, + xhr: true + assert_response :forbidden + end + + def test_log_member_local_wf + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf_workflows/#{@wf1.id}/log", + params: { project_id: @project1.id, dmsf_file_id: @file1.id, format: 'js' }, + xhr: true + assert_response :success + assert_template :log + end + + 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' }, + xhr: true + assert_response :success + assert_template :log + end + + def test_log_admin + post '/login', params: { username: 'admin', password: 'admin' } + get "/dmsf_workflows/#{@wf1.id}/log", + params: { project_id: @project1.id, dmsf_file_id: @file1.id, format: 'js' }, + xhr: true + assert_response :success + assert_template :log + end + + def test_new_step + post '/login', params: { username: 'jsmith', password: 'jsmith' } + get "/dmsf_workflows/#{@wf1.id}/new_step", params: { format: 'js' }, xhr: true + assert_response :success + assert_template :new_step + end +end diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb new file mode 100644 index 00000000..d459bf16 --- /dev/null +++ b/test/functional/issues_controller_test.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Issues controller +class IssuesControllerTest < RedmineDmsf::Test::TestCase + def setup + super + @issue1 = Issue.find 1 + post '/login', params: { username: 'jsmith', password: 'jsmith' } + end + + def test_put_update_with_project_change + # If we move an issue to a different project, attached documents and their system folders must be moved too + assert_equal @issue1.project, @project1 + system_folder = @issue1.system_folder + assert system_folder + assert_equal @project1.id, system_folder.project_id + main_system_folder = @issue1.main_system_folder + assert main_system_folder + assert_equal @project1.id, main_system_folder.project_id + patch "/issues/#{@issue1.id}", + params: { issue: { project_id: @project2.id, tracker_id: '1', priority_id: '6', category_id: '3' } } + assert_redirected_to action: 'show', id: @issue1.id + @issue1.reload + assert_equal @project2.id, @issue1.project.id + system_folder = @issue1.system_folder + assert system_folder + assert_equal @project2.id, system_folder.project_id + main_system_folder = @issue1.main_system_folder + assert main_system_folder + assert_equal @project2.id, main_system_folder.project_id + end +end diff --git a/test/functional/my_controller_test.rb b/test/functional/my_controller_test.rb new file mode 100644 index 00000000..953e7df9 --- /dev/null +++ b/test/functional/my_controller_test.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# My controller +class MyControllerTest < RedmineDmsf::Test::TestCase + include Redmine::I18n + + def test_page_with_open_approvals_one_approval + post '/login', params: { username: 'jsmith', password: 'jsmith' } + DmsfFileRevision.where(id: 5).delete_all + @jsmith.pref[:my_page_layout] = { 'top' => ['open_approvals'] } + @jsmith.pref.save! + get '/my/page' + assert_response :success + assert_select 'div#list-top' do + assert_select 'h3', { text: "#{l(:open_approvals)} (1)" } + end + 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! + get '/my/page' + assert_response :success + assert_select 'div#list-top' do + assert_select 'h3', { text: "#{l(:open_approvals)} (0)" } + end + end + + def test_page_with_open_locked_documents + post '/login', params: { username: 'admin', password: 'admin' } + @request.session[:user_id] = @admin.id + @admin.pref[:my_page_layout] = { 'top' => ['locked_documents'] } + @admin.pref.save! + get '/my/page' + assert_response :success + text = l(:locked_documents) + text << " (0 #{l(:label_number_of_folders).downcase} / 1 #{l(:label_number_of_documents).downcase})" + assert_select 'div#list-top' do + assert_select 'h3', { text: text } + end + end + + def test_page_with_open_watched_documents + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @jsmith.pref[:my_page_layout] = { 'top' => ['watched_documents'] } + @jsmith.pref.save! + @file1.add_watcher @jsmith + @folder1.add_watcher @jsmith + @project1.add_watcher @jsmith + get '/my/page' + assert_response :success + assert_select 'div#list-top' do + assert_select 'h3', { text: "#{l(:label_dmsf_watched)} (2/1)" } + end + end +end diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb new file mode 100644 index 00000000..e88c391e --- /dev/null +++ b/test/functional/projects_controller_test.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Projects controller +class ProjectsControllerTest < RedmineDmsf::Test::TestCase + include Redmine::I18n + + def test_settings_dms_member + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.add_permission! :user_preferences + with_settings plugin_redmine_dmsf: { 'dmsf_act_as_attachable' => '1' } do + get "/projects/#{@project1.id}/settings", params: { tab: 'dmsf' } + end + assert_response :success + assert_select 'fieldset legend', text: l(:link_user_preferences) + assert_select 'fieldset legend', text: "#{l(:field_project)} #{l(:label_preferences)}" + end + + def test_settings_dms_member_no_permission + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.remove_permission! :user_preferences + get "/projects/#{@project1.id}/settings", params: { tab: 'dmsf' } + assert_response :success + assert_select 'fieldset legend', text: l(:link_user_preferences), count: 0 + end + + def test_settings_dms_non_member + post '/login', params: { username: 'admin', password: 'admin' } + get "/projects/#{@project1.id}/settings", params: { tab: 'dmsf' } + assert_response :success + assert_select 'fieldset legend', text: l(:link_user_preferences), count: 0 + end + + def test_settings_dms_member_no_act_as_attachments + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.add_permission! :user_preferences + get "/projects/#{@project1.id}/settings", params: { tab: 'dmsf' } + assert_response :success + assert_select 'label', text: l(:label_act_as_attachable), count: 0 + end + + def test_legacy_notifications + post '/login', params: { username: 'jsmith', password: 'jsmith' } + @role_manager.add_permission! :user_preferences + with_settings notified_events: ['dmsf_legacy_notifications'] do + get "/projects/#{@project1.id}/settings", params: { tab: 'dmsf' } + assert_response :success + assert_select 'label', text: l(:label_notifications) + end + end +end diff --git a/test/helper_test.rb b/test/helper_test.rb new file mode 100644 index 00000000..4fb38e94 --- /dev/null +++ b/test/helper_test.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Test + # Helper test + class HelperTest < ActiveSupport::TestCase + def initialize(name) + super + # Load all plugin's fixtures + dir = File.join(File.dirname(__FILE__), 'fixtures') + ext = '.yml' + Dir.glob("#{dir}/**/*#{ext}").each do |file| + fixture = File.basename(file, ext) + ActiveRecord::FixtureSet.create_fixtures dir, fixture + end + end + + def setup + @jsmith = User.find 2 + @manager_role = Role.find_by(name: 'Manager') + @developer_role = Role.find_by(name: 'Developer') + [@manager_role, @developer_role].each do |role| + role.add_permission! :view_dmsf_folders + role.add_permission! :view_dmsf_files + end + @project1 = Project.find 1 + @project2 = Project.find 2 + [@project1, @project2].each do |prj| + prj.enable_module! :dmsf + end + Setting.plugin_redmine_dmsf['dmsf_storage_directory'] = File.join('files', 'dmsf') + Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = nil + Setting.plugin_redmine_dmsf['dmsf_projects_as_subfolders'] = nil + FileUtils.cp_r File.join(File.expand_path('../fixtures/files', __FILE__), '.'), DmsfFile.storage_path + end + + def teardown + # Delete our tmp folder + FileUtils.rm_rf DmsfFile.storage_path + rescue StandardError => e + Rails.logger.error e.message + end + end + end +end diff --git a/test/helpers/dmsf_files_helper_test.rb b/test/helpers/dmsf_files_helper_test.rb new file mode 100644 index 00000000..0c118c1b --- /dev/null +++ b/test/helpers/dmsf_files_helper_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# DMSF helper +class DmsfFilesHelperTest < RedmineDmsf::Test::HelperTest + include DmsfFilesHelper + + def test_clean_wiki_test + text = "

          xxx

          \n\n\n\t" + assert_equal 'xxx

          ', clean_wiki_text(text) + end +end diff --git a/test/helpers/dmsf_helper_test.rb b/test/helpers/dmsf_helper_test.rb new file mode 100644 index 00000000..1c8dac4a --- /dev/null +++ b/test/helpers/dmsf_helper_test.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# DMSF helper +class DmsfHelperTest < RedmineDmsf::Test::HelperTest + include DmsfHelper + + def setup + super + @folder1 = DmsfFolder.find 1 + end + + def test_webdav_url + base_url = ["#{Setting.protocol}:/", Setting.host_name, 'dmsf', 'webdav'].join('/') + assert_equal "#{base_url}/", webdav_url(nil, nil) + assert_equal "#{base_url}/%5Becookbook%5D/", webdav_url(@project1, nil) + assert_equal "#{base_url}/%5Becookbook%5D/folder1/", webdav_url(@project1, @folder1) + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1' } do + assert_equal "#{base_url}/%5BeCookbook%201%5D/", webdav_url(@project1, nil) + end + end +end diff --git a/test/helpers/dmsf_links_helper_test.rb b/test/helpers/dmsf_links_helper_test.rb new file mode 100644 index 00000000..b37ab925 --- /dev/null +++ b/test/helpers/dmsf_links_helper_test.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Links helper +class DmsfLinksHelperTest < RedmineDmsf::Test::HelperTest + include DmsfLinksHelper + + def test_number + assert DmsfLinksHelper.number?('123') + assert_not DmsfLinksHelper.number?('-123') + assert_not DmsfLinksHelper.number?('12.3') + assert_not DmsfLinksHelper.number?('123a') + assert_not DmsfLinksHelper.number?(nil) + end + + def test_default_folder_id_in_files_for_select + project = Project.find(1) + assert_nothing_raised do + files_for_select(project.id) + end + end +end diff --git a/test/helpers/dmsf_queries_helper_test.rb b/test/helpers/dmsf_queries_helper_test.rb new file mode 100644 index 00000000..78854535 --- /dev/null +++ b/test/helpers/dmsf_queries_helper_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Queries helper +class DmsfQueriesHelperTest < RedmineDmsf::Test::HelperTest + include DmsfQueriesHelper + include ActionView::Helpers::NumberHelper + include ActionView::Helpers::TagHelper + + def setup + @folder1 = DmsfFolder.find 1 + super + end + + def test_csv_value + # Size + column = QueryColumn.new(:size) + assert_equal '1 KB', csv_value(column, nil, 1024) + # Author + column = QueryColumn.new(:author) + assert_equal 'John Smith', csv_value(column, @jsmith, @jsmith.id) + end + + def test_column_value + # Size + column = QueryColumn.new(:size) + assert_equal '1 KB', column_value(column, @folder1, 1024) + # Comment + column = QueryColumn.new(:comment) + assert column_value(column, @folder1, '*Comment*').include?('wiki') + end + + def test_previewable + assert previewable?('file.txt', 'text/plain') + assert previewable?('main.c', 'text/x-csrc') + assert_not previewable?('document.odt', 'application/vnd.oasis.opendocument.text') + end +end diff --git a/test/integration/rest_api/dmsf_api_test.rb b/test/integration/rest_api/dmsf_api_test.rb new file mode 100644 index 00000000..192c710a --- /dev/null +++ b/test/integration/rest_api/dmsf_api_test.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# DMSF API +class DmsfFileApiTest < RedmineDmsf::Test::IntegrationTest + include Redmine::I18n + + def setup + super + Setting.rest_api_enabled = '1' + @token = Token.create!(user: @jsmith_user, action: 'api') + end + + def test_get + # curl -v -H "Content-Type: application/xml" -X GET -u ${1}:${2} http://localhost:3000/projects/12/dmsf.xml + get "/projects/#{@project1.id}/dmsf.xml?key=#{@token.value}" + assert_response :success + assert @response.media_type.include?('application/xml') + # + # + # + # + # 1 + # folder1 + # folder + # + # + # 1 + # folder1_link + # folder-link + # 1 + # 1 + # + # + # 6 + # folder6 + # folder + # + # + # 7 + # folder7 + # folder + # + # + # 6 + # file1_link + # file-link + # 1 + # 1 + # + # + # 9 + # My File + # file + # myfile.txt + # + # + # 8 + # PDF + # file + # test.pdf + # + # + # 1 + # Test File + # file + # test5.txt + # + # + # 4 + # test_link + # file-link + # 4 + # 1 + # + # + # 10 + # Zero Size File + # file + # zero.txt + # + # + # + assert_select 'node > id', text: @folder1.id.to_s + assert_select 'node > title', text: @folder1.title + assert_select 'node > type', text: 'folder' + assert_select 'node > filename', text: @file9.last_revision.name + assert_select 'node > target_id', text: @folder_link1.target_id.to_s + assert_select 'node > target_project_id', text: @folder_link1.target_project_id.to_s + end + + def test_copy_entries + # curl -v -H "Content-Type: application/xml" -X POST --data "@entries.xml" -H "X-Redmine-API-Key: ${USER_API_KEY}" \ + # "http://localhost:3000/projects/3342/dmsf/entries.xml?ids[]=file-254566©_entries=true" + payload = %( + + #{@project1.id} + #{@folder1.id} + + ) + assert_difference('@folder1.dmsf_files.count', 1) do + post "/projects/#{@project1.id}/dmsf/entries.xml?ids[]=file-#{@file1.id}©_entries=true&key=#{@token.value}", + params: payload, + headers: { 'CONTENT_TYPE' => 'application/xml' } + end + assert_response :redirect + end + + def test_move_entries + # curl -v -H "Content-Type: application/xml" -X POST --data "@entries.xml" -H "X-Redmine-API-Key: ${USER_API_KEY}" \ + # "http://localhost:3000/projects/3342/dmsf/entries.xml?ids[]=file-254566&move_entries=true" + payload = %( + + #{@project1.id} + #{@folder1.id} + + ) + assert_difference('@folder1.dmsf_files.count', 1) do + post "/projects/#{@project1.id}/dmsf/entries.xml?ids[]=file-#{@file1.id}&move_entries=true&key=#{@token.value}", + params: payload, + headers: { 'CONTENT_TYPE' => 'application/xml' } + end + assert_response :redirect + end + + def test_download_entries + # curl -v -H "Content-Type: application/xml" -X POST --data "" -H "X-Redmine-API-Key: ${USER_API_KEY}" \ + # "http://localhost:3000/projects/3342/dmsf/entries.xml?ids[]=file-254566" + post "/projects/#{@project1.id}/dmsf/entries.xml?ids[]=file-#{@file1.id}&key=#{@token.value}", + params: '', + headers: { 'CONTENT_TYPE' => 'application/octet-stream' } + assert_response :success + end + + def test_delete_entries + # curl -v -H "Content-Type: application/xml" -X POST --data "" -H "X-Redmine-API-Key: ${USER_API_KEY}" \ + # "http://localhost:3000/projects/3342/dmsf/entries.xml?ids[]=file-254566&delete_entries=true" + assert_difference('@project1.dmsf_files.visible.count', -1) do + post "/projects/#{@project1.id}/dmsf/entries.xml?ids[]=file-#{@file1.id}&delete_entries=true&key=#{@token.value}", + params: '', + headers: { 'CONTENT_TYPE' => 'application/xml' } + end + assert_response :redirect + end +end diff --git a/test/integration/rest_api/dmsf_file_api_test.rb b/test/integration/rest_api/dmsf_file_api_test.rb new file mode 100644 index 00000000..37a42fb2 --- /dev/null +++ b/test/integration/rest_api/dmsf_file_api_test.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# File API +class DmsfFileApiTest < RedmineDmsf::Test::IntegrationTest + include Redmine::I18n + + def setup + super + Setting.rest_api_enabled = '1' + @token = Token.create!(user: @jsmith_user, action: 'api') + end + + def test_get_document + # curl -v -H "Content-Type: application/xml" -X GET -u ${1}:${2} http://localhost:3000/dmsf/files/17216.xml + get "/dmsf/files/#{@file1.id}.xml?key=#{@token.value}" + assert_response :success + assert @response.media_type.include?('application/xml') + # + # + # 1 + # Test File + # test.txt + # 1 + # http://www.example.com/dmsf/files/1/download + # + # + # 5 + # + # test5.txt + # http://www.example.com/dmsf/files/1/view?download=5 + # 4 + # text/plain + # Test File + # + # 1 + # 1.0 + # + # 1 + # 2017-04-18T12:52:28Z + # 2019-01-15T15:56:15Z + # + # + # + # + # + # Waiting for Approval + # + # + # + # 1 + # + # test.txt + # http://www.example.com/dmsf/files/1/view?download=1 + # 4 + # text/plain + # Test File + # Some file :-) + # 1 + # 1.0 + # + # 1 + # 2017-04-18T12:52:27Z + # 2019-01-15T15:56:15Z + # + # 1 + # + # 1 + # + # Waiting for Approval + # 81dc9bdb52d04dc20036dbd8313ed055 + # + # + # ... + # + # + # + # + # + assert_select 'dmsf_file > id', text: @file1.id.to_s + assert_select 'dmsf_file > title', text: @file1.title + assert_select 'dmsf_file > name', text: @file1.name + assert_select 'dmsf_file > project_id', text: @file1.project_id.to_s + assert_select 'dmsf_file > content_url', text: "http://www.example.com/dmsf/files/#{@file1.id}/download" + assert_select 'dmsf_file > dmsf_file_revisions > dmsf_file_revision', @file1.dmsf_file_revisions.all.size + assert_select 'dmsf_file > dmsf_file_revisions > dmsf_file_revision > custom_fields > custom_field' + assert_select 'dmsf_file > dmsf_file_revisions > dmsf_file_revision > dmsf_worklfow_state', + text: 'Waiting for Approval' + # curl -v -H "Content-Type: application/octet-stream" -X GET -u ${1}:${2} + # http://localhost:3000/dmsf/files/41532/download > file.txt + get "/dmsf/files/#{@file1.id}/download.xml?key=#{@token.value}" + assert_response :success + assert_equal '123', @response.body + end + + def test_upload_document + # curl --data-binary "@cat.gif" -H "Content-Type: application/octet-stream" -X POST -u ${1}:${2} + # http://localhost:3000/projects/12/dmsf/upload.xml?filename=cat.gif + post "/projects/#{@project1.id}/dmsf/upload.xml?filename=test.txt&key=#{@token.value}", + params: 'File content', + headers: { 'CONTENT_TYPE' => 'application/octet-stream' } + assert_response :created + assert @response.media_type.include?('application/xml') + # + # + # 2.8bb2564936980e92ceec8a5759ec34a8 + # + xml = Hash.from_xml(response.body) + assert_kind_of Hash, xml['upload'] + ftoken = xml['upload']['token'] + assert_not_nil ftoken + # curl -v -H "Content-Type: application/xml" -X POST --data "@file.xml" -u ${1}:${2} + # http://localhost:3000/projects/12/dmsf/commit.xml + payload = %( + + + test.txt + test.txt + + REST API + From API + A + 1 + 0 + + #{ftoken} + + ) + assert_difference 'DmsfFileRevision.count', +1 do + post "/projects/#{@project1.id}/dmsf/commit.xml?key=#{@token.value}", + params: payload, + headers: { 'CONTENT_TYPE' => 'application/xml' } + end + # + # + # + # 17229 + # test.txt + # + # # + assert_select 'dmsf_files > file > name', text: 'test.txt' + assert_response :success + revision = DmsfFileRevision.order(:created_at).last + assert revision.present? + assert_equal 'text/plain', revision.mime_type + end + + def test_upload_document_exceeded_attachment_max_size + Setting.attachment_max_size = '1' + # curl --data-binary "@text.txt" -H "Content-Type: application/octet-stream" -X POST -u ${1}:${2} + # http://localhost:3000/projects/12/dmsf/upload.xml?filename=text.txt + file_content = 'x' * 2.kilobytes + post "/projects/#{@project1.id}/dmsf/upload.xml?filename=test.txt&key=#{@token.value}", + params: file_content, + headers: { 'CONTENT_TYPE' => 'application/octet-stream' } + assert_response :unprocessable_entity + end + + def test_delete_file + # curl -v -H "Content-Type: application/xml" -X DELETE -u ${1}:${2} http://localhost:3000/dmsf/files/196118.xml + delete "/dmsf/files/#{@file1.id}.xml?key=#{@token.value}", headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :success + @file1.reload + assert_equal DmsfFile::STATUS_DELETED, @file1.deleted + assert_equal @jsmith_user, @file1.deleted_by_user + end + + def test_delete_file_no_permissions + @role.remove_permission! :file_delete + token = Token.create!(user: @jsmith_user, action: 'api') + # curl -v -H "Content-Type: application/xml" -X DELETE -u ${1}:${2} http://localhost:3000/dmsf/files/196118.xml + delete "/dmsf/files/#{@file1.id}.xml?key=#{token.value}", headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :forbidden + end + + def test_delete_folder_commit_yes + # curl -v -H "Content-Type: application/xml" -X DELETE -u ${1}:${2} + # http://localhost:3000/dmsf/files/196118.xml&commit=yes + delete "/dmsf/files/#{@file1.id}.xml?key=#{@token.value}&commit=yes", + headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :success + assert_nil DmsfFile.find_by(id: @file1.id) + end + + def test_delete_file_locked + User.current = @admin_user + @file1.lock! + User.current = nil + # curl -v -H "Content-Type: application/xml" -X DELETE -u ${1}:${2} http://localhost:3000/dmsf/files/196118.xml + delete "/dmsf/files/#{@file1.id}.xml?key=#{@token.value}", headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :unprocessable_content + # + # + # Locked by Admin + # + assert_select 'errors > error', text: l(:title_locked_by_user, user: @admin_user.name) + @file1.reload + assert_equal DmsfFile::STATUS_ACTIVE, @file1.deleted + end + + def test_create_revision + # curl -v -H "Content-Type: application/xml" -X POST --data "@revision.xml" -u ${1}:${2} + # http://localhost:3000/dmfs/files/1/revision/create.xml + payload = %( + + #{@file1.name} + #{@file1.name} + SQL script + REST API + + User documentation + + + ) + post "/dmsf/files/#{@file1.id}/revision/create.xml?key=#{@token.value}", + params: payload, + headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :success + # + # + # 293566 + # + assert_select 'dmsf_file_revision > id', text: @file1.last_revision.id.to_s + end +end diff --git a/test/integration/rest_api/dmsf_folder_api_test.rb b/test/integration/rest_api/dmsf_folder_api_test.rb new file mode 100644 index 00000000..6e12addc --- /dev/null +++ b/test/integration/rest_api/dmsf_folder_api_test.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# Folder API +class DmsfFolderApiTest < RedmineDmsf::Test::IntegrationTest + include Redmine::I18n + + def setup + super + Setting.rest_api_enabled = '1' + @token = Token.create!(user: @jsmith_user, action: 'api') + end + + def test_list_folder + # curl -v -H "Content-Type: application/xml" -X GET -u ${1}:${2} http://localhost:3000/dmsf/files/17216.xml + get "/projects/#{@project1.identifier}/dmsf.xml?key=#{@token.value}" + assert_response :success + assert @response.media_type.include?('application/xml') + # + # + # + # + # 1 + # folder1 + # folder + # + # ... + # + # + assert_select 'dmsf > dmsf_nodes > node > id', text: @folder1.id.to_s + assert_select 'dmsf > dmsf_nodes > node > title', text: @folder1.title + assert_select 'dmsf > dmsf_nodes > node > type', text: 'folder' + end + + def test_list_folder_with_sub_projects + with_settings plugin_redmine_dmsf: { 'dmsf_projects_as_subfolders' => '1' } do + # curl -v -H "Content-Type: application/xml" -X GET -u ${1}:${2} http://localhost:3000/dmsf/files/17216.xml + get "/projects/#{@project1.identifier}/dmsf.xml?key=#{@token.value}" + assert_response :success + assert @response.media_type.include?('application/xml') + # + # + # + # + # 3 + # eCookbook Subproject 1 + # project + # + # ... + # + # + # @project5 is as a sub-folder + assert_select 'dmsf > dmsf_nodes > node > id', text: @project5.id.to_s + assert_select 'dmsf > dmsf_nodes > node > title', text: @project5.name + assert_select 'dmsf > dmsf_nodes > node > type', text: 'project' + end + end + + def test_list_folder_limit_and_offset + # curl -v -H "Content-Type: application/xml" -X GET -u ${1}:${2} + # "http://localhost:3000/dmsf/files/17216.xml?limit=1&offset=1" + get "/projects/#{@project1.identifier}/dmsf.xml?key=#{@token.value}&limit=1&offset=2" + assert_response :success + assert @response.media_type.include?('application/xml') + # + # + # + # + # 7 + # folder7 + # folder + # + # + # + # + assert_select 'dmsf > dmsf_nodes > node', count: 1 + # assert_select 'dmsf > dmsf_nodes > node > id', text: @folder7.id.to_s + # assert_select 'dmsf > dmsf_nodes > node > title', text: @folder7.title + end + + def test_create_folder + # curl -v -H "Content-Type: application/xml" -X POST --data "@folder.xml" -u ${1}:${2} + # http://localhost:3000/projects/12/dmsf/create.xml + payload = %( + rest_api + A folder created via REST API + + ) + post "/projects/#{@project1.identifier}/dmsf/create.xml?key=#{@token.value}", + params: payload, + headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :success + # + # + # 8 + # rest_api + # + assert_select 'dmsf_folder > title', text: 'rest_api' + end + + def test_create_subfolder + # curl -v -H "Content-Type: application/xml" -X POST --data "@folder.xml" -u ${1}:${2} + # http://localhost:3000/projects/12/dmsf/create.xml + payload = %( + rest_api + A folder created via REST API + #{@folder1.id} + ) + post "/projects/#{@project1.identifier}/dmsf/create.xml?key=#{@token.value}", + params: payload, + headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :success + # + # + # 8 + # rest_api + # + assert_select 'dmsf_folder > title', text: 'rest_api' + assert @folder1.dmsf_folders.exists?(title: 'rest_api') + end + + def test_find_folder_by_title + # curl -v -H "Content-Type: application/json" -X GET + # -H "X-Redmine-API-Key: USERS_API_KEY" http://localhost:3000/projects/1/dmsf.json?folder_title=Updated%20title + get "/projects/#{@project1.identifier}/dmsf.xml?key=#{@token.value}&folder_title=#{@folder1.title}" + assert_response :success + assert @response.media_type.include?('application/xml') + # + # + # + # + # 2 + # folder2 + # + # + # + # + # + # + # + # 1 + # folder1 + # + # + assert_select 'dmsf > found_folder > id', text: @folder1.id.to_s + assert_select 'dmsf > found_folder > title', text: @folder1.title + end + + def test_find_folder_by_title_not_found + # curl -v -H "Content-Type: application/json" -X GET + # -H "X-Redmine-API-Key: USERS_API_KEY" http://localhost:3000/projects/1/dmsf.json?folder_title=Updated%20title + get "/projects/#{@project1.identifier}/dmsf.xml?key=#{@token.value}&folder_title=xxx" + assert_response :not_found + end + + def test_find_folder_by_id + # curl -v -H "Content-Type: application/json" -X GET + # -H "X-Redmine-API-Key: USERS_API_KE" http://localhost:3000/projects/1/dmsf.json?folder_id=3 + get "/projects/#{@project1.identifier}/dmsf.xml?key=#{@token.value}&folder_id=#{@folder1.id}" + assert_response :success + assert @response.media_type.include?('application/xml') + # + # + # + # + # 2 + # folder2 + # folder + # + # + # 2 + # test_link + # file-link + # 4 + # 1 + # + # + # 5 + # url_link + # url-link + # https://www.kontron.com + # + # + # + # 1 + # folder1 + # + # + # User documentation + # + # + # + # + assert_select 'dmsf > found_folder > id', text: @folder1.id.to_s + assert_select 'dmsf > found_folder > title', text: @folder1.title + assert_select 'dmsf > found_folder > custom_fields > custom_field' + end + + def test_find_folder_by_id_not_found + # curl -v -H "Content-Type: application/json" -X GET -H "X-Redmine-API-Key: USERS_API_KE" + # http://localhost:3000/projects/1/dmsf.json?folder_id=3 + get "/projects/#{@project1.identifier}/dmsf.xml?key=#{@token.value}&folder_id=none" + assert_response :not_found + end + + def test_update_folder + # curl -v -H "Content-Type: application/json" -X POST --data "@update-folder-payload.json" + # -H "X-Redmine-API-Key: USERS_API_KEY" http://localhost:3000//projects/#{project_id}/dmsf/save.json + payload = %( + rest_api + A folder updated via REST API + ) + post "/projects/#{@project1.identifier}/dmsf/save.xml?folder_id=1&key=#{@token.value}", + params: payload, headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :success + # + # + # 1 + # rest_api + # A folder updated via REST API + # + assert_select 'dmsf_folder > title', text: 'rest_api' + end + + def test_delete_folder + # curl -v -H "Content-Type: application/xml" -X DELETE -u ${1}:${2} + # http://localhost:3000/projects/1/dmsf/delete.xml?folder_id=3 + delete "/projects/#{@folder6.project.identifier}/dmsf/delete.xml?key=#{@token.value}&folder_id=#{@folder6.id}", + headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :success + @folder6.reload + assert_equal DmsfFolder::STATUS_DELETED, @folder6.deleted + assert_equal @jsmith_user, @folder6.deleted_by_user + end + + def test_delete_folder_no_permission + @role.remove_permission! :folder_manipulation + # curl -v -H "Content-Type: application/xml" -X DELETE -u ${1}:${2} + # http://localhost:3000/projects/1/dmsf/delete.xml?folder_id=3 + delete "/projects/#{@folder6.project.identifier}/dmsf/delete.xml?key=#{@token.value}&folder_id=#{@folder6.id}", + headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :forbidden + end + + def test_delete_folder_commit_yes + # curl -v -H "Content-Type: application/xml" -X DELETE -u ${1}:${2} + # http://localhost:3000/projects/1/dmsf/delete.xml?folder_id=3 + url = + "/projects/#{@folder6.project.identifier}/dmsf/delete.xml?key=#{@token.value}&folder_id=#{@folder6.id}&commit=yes" + delete url, + headers: { CONTENT_TYPE: 'application/xml' } + assert_response :success + assert_nil DmsfFolder.find_by(id: @folder6.id) + end + + def test_delete_folder_locked + # curl -v -H "Content-Type: application/xml" -X DELETE -u ${1}:${2} + # http://localhost:3000/projects/1/dmsf/delete.xml?folder_id=3 + delete "/projects/#{@folder2.project.identifier}/dmsf/delete.xml?key=#{@token.value}&folder_id=#{@folder2.id}", + headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :unprocessable_content + # + # + # Folder is locked + # + assert_select 'errors > error', text: l(:error_folder_is_locked) + @folder2.reload + assert_equal DmsfFolder::STATUS_ACTIVE, @folder2.deleted + end +end diff --git a/test/integration/rest_api/dmsf_link_api_test.rb b/test/integration/rest_api/dmsf_link_api_test.rb new file mode 100644 index 00000000..3d2c54bc --- /dev/null +++ b/test/integration/rest_api/dmsf_link_api_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# Link API +class DmsfLinkApiTest < RedmineDmsf::Test::IntegrationTest + include Redmine::I18n + + def setup + super + Setting.rest_api_enabled = '1' + end + + def test_create_link + token = Token.create!(user: @jsmith_user, action: 'api') + name = 'REST API link test' + # curl -v -H "Content-Type: application/xml" -X POST --data "@link.xml" + # -H "X-Redmine-API-Key: USERS_API_KEY" http://localhost:3000/dmsf_links.xml + payload = %( + #{@project1.id} + link_from + + #{@project1.id} + Documents + #{@file1.id} + + #{name} + ) + post "/dmsf_links.xml?key=#{token.value}", params: payload, headers: { 'CONTENT_TYPE' => 'application/xml' } + assert_response :success + # + # + # 1243 + # test + # + assert_select 'dmsf_link > title', text: name + assert_equal 1, DmsfLink.where(name: name, project_id: @project1.id).count + end +end diff --git a/test/integration/webdav/dmsf_webdav_custom_middleware_test.rb b/test/integration/webdav/dmsf_webdav_custom_middleware_test.rb new file mode 100644 index 00000000..c0e67436 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_custom_middleware_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# Custom middleware test +class DmsfWebdavCustomMiddlewareTest < RedmineDmsf::Test::IntegrationTest + def test_options_for_root_path + process :options, '/' + assert_response :method_not_allowed + end + + def test_options_for_dmsf_root_path + process :options, '/dmsf' + assert_response :method_not_allowed + end + + def test_propfind_for_root_path + process :propfind, '/' + assert_response :method_not_allowed + end + + def test_propfind_for_dmsf_root_path + process :propfind, '/dmsf' + assert_response :method_not_allowed + end + + def test_webdav_not_enabled + with_settings plugin_redmine_dmsf: { 'dmsf_webdav' => nil } do + process :options, '/dmsf/webdav' + assert_response :not_found + end + end + + def test_webdav_enabled + process :options, '/dmsf/webdav' + assert_response :success + end +end diff --git a/test/integration/webdav/dmsf_webdav_delete_test.rb b/test/integration/webdav/dmsf_webdav_delete_test.rb new file mode 100644 index 00000000..4cc72f60 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_delete_test.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# WebDAV delete test +class DmsfWebdavDeleteTest < RedmineDmsf::Test::IntegrationTest + include Redmine::I18n + + def test_not_authenticated + delete '/dmsf/webdav' + assert_response :unauthorized + end + + def test_not_authenticated_project + delete "/dmsf/webdav/#{@project1.identifier}" + assert_response :unauthorized + end + + def test_failed_authentication_global + delete '/dmsf/webdav', params: nil, headers: credentials('admin', 'badpassword') + assert_response :unauthorized + end + + def test_failed_authentication + delete "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: credentials('admin', 'badpassword') + assert_response :unauthorized + end + + def test_root_folder + delete '/dmsf/webdav', params: nil, headers: @admin + assert_response :not_implemented + end + + def test_delete_not_empty_folder + put "/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}", params: nil, headers: @admin + assert_response :forbidden + end + + def test_not_existed_project + delete '/dmsf/webdav/not_a_project/file.txt', params: nil, headers: @admin + assert_response :conflict + end + + def test_dmsf_not_enabled + @project1.disable_module! :dmsf + delete "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @jsmith + assert_response :not_found # Item does not exist, as project is not enabled. + end + + def test_delete_when_ro + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_strategy' => 'WEBDAV_READ_ONLY', 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic' } do + delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @admin + assert_response :bad_gateway # WebDAV is read only + end + end + + def test_unlocked_file + delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @admin + assert_response :no_content + @file1.reload + assert @file1.deleted?, "File #{@file1.name} hasn't been deleted" + end + + def test_unathorized_user + @role.remove_permission! :view_dmsf_folders + delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @jsmith + assert_response :not_found # Without folder_view permission, he will not even be aware of its existence. + @file1.reload + assert_not @file1.deleted?, "File #{@file1.name} is expected to exist" + end + + def test_unathorized_user_forbidden + @role.remove_permission! :file_delete + delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @jsmith + assert_response :forbidden + @file1.reload + assert_not @file1.deleted?, "File #{@file1.name} is expected to exist" + end + + def test_view_folder_not_allowed + @role.remove_permission! :view_dmsf_folders + delete "/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}", params: nil, headers: @jsmith + assert_response :not_found # Without folder_view permission, he will not even be aware of its existence. + @folder1.reload + assert_not @folder1.deleted?, "Folder #{@folder1.title} is expected to exist" + end + + def test_folder_manipulation_not_allowed + @role.remove_permission! :folder_manipulation + delete "/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}", params: nil, headers: @jsmith + assert_response :forbidden # Without manipulation permission, action is forbidden. + @folder1.reload + assert_not @folder1.deleted?, "Foler #{@folder1.title} is expected to exist" + end + + def test_folder_delete_by_admin + delete "/dmsf/webdav/#{@project1.identifier}/#{@folder6.title}", params: nil, headers: @admin + assert_response :success + @folder6.reload + assert @folder6.deleted?, "Folder #{@folder6.title} is not expected to exist" + end + + def test_folder_delete_by_user + delete "/dmsf/webdav/#{@project1.identifier}/#{@folder6.title}", params: nil, headers: @jsmith + assert_response :success + @folder6.reload + assert @folder6.deleted?, "Folder #{@folder1.title} is not expected to exist" + end + + def test_folder_delete_by_user_with_project_names + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + delete "/dmsf/webdav/#{@project1.identifier}/#{@folder6.title}", params: nil, headers: @jsmith + assert_response :conflict + p1name_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + delete "/dmsf/webdav/#{p1name_uri}/#{@folder6.title}", params: nil, headers: @jsmith + assert_response :success + @folder6.reload + assert @folder6.deleted?, "Folder #{@folder6.title} is not expected to exist" + end + end + + def test_file_delete_by_administrator + delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @admin + assert_response :success + @file1.reload + assert @file1.deleted?, "File #{@file1.name} is not expected to exist" + end + + def test_file_delete_by_user + delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @jsmith + assert_response :success + @file1.reload + assert @file1.deleted?, "File #{@file1.name} is not expected to exist" + end + + def test_file_delete_by_user_with_project_names + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @jsmith + assert_response :conflict + p1name_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + delete "/dmsf/webdav/#{p1name_uri}/#{@file1.name}", params: nil, headers: @jsmith + assert_response :success + @file1.reload + assert @file1.deleted?, "File #{@file1.name} is not expected to exist" + end + end + + def test_folder_delete_fragments + delete "/dmsf/webdav/#{@project1.identifier}/#{@folder6.title}/#frament=HTTP/1.1", params: nil, headers: @jsmith + assert_response :bad_request + end + + def test_locked_folder + @folder6.lock! + delete "/dmsf/webdav/#{@project1.identifier}/#{@folder6.title}", params: nil, headers: @jsmith + assert_response :locked + @folder6.reload + assert_not @folder6.deleted?, "Folder #{@folder6.title} is expected to exist" + end + + def test_locked_file + @file1.lock! + delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @jsmith + assert_response :locked + @file1.reload + assert_not @file1.deleted?, "File #{@file1.name} is expected to exist" + end + + def test_non_versioned_file + delete "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", params: nil, headers: @jsmith + assert_response :success + # The file should be destroyed + assert_nil DmsfFile.visible.find_by(id: @file1.id) + end + + def test_delete_file_in_subproject + delete "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@file12.name}", params: nil, headers: @admin + assert_response :success + end + + def test_delete_folder_in_subproject + delete "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@folder10.title}", + params: nil, + headers: @admin + assert_response :success + end + + def test_delete_folder_in_subproject_brackets + project3_uri = Addressable::URI.encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project5)) + project1_uri = Addressable::URI.encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + delete "/dmsf/webdav/#{project1_uri}/#{project3_uri}/#{@folder10.title}", params: nil, headers: @admin + assert_response :success + end + + def test_delete_subproject + delete "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}", params: nil, headers: @admin + assert_response :method_not_allowed + end +end diff --git a/test/integration/webdav/dmsf_webdav_get_test.rb b/test/integration/webdav/dmsf_webdav_get_test.rb new file mode 100644 index 00000000..40b8087b --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_get_test.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# WebDAV GET test +class DmsfWebdavGetTest < RedmineDmsf::Test::IntegrationTest + def test_should_deny_anonymous + get '/dmsf/webdav' + assert_response :unauthorized + end + + def test_should_deny_failed_authentication + get '/dmsf/webdav', params: nil, headers: credentials('admin', 'badpassword') + assert_response :unauthorized + end + + def test_digest_authentication + # Basic + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_authentication' => 'Basic', 'dmsf_webdav' => '1' } do + get '/dmsf/webdav', params: nil, headers: credentials('jsmith', 'jsmith') + assert_response :success + end + # Wrong digest + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_authentication' => 'Digest', 'dmsf_webdav' => '1' } do + get '/dmsf/webdav', params: nil, headers: credentials('jsmith', 'jsmith') + assert_response :unauthorized + end + # Right digest + digest = ActiveSupport::Digest.hexdigest( + "#{@jsmith_user.login}:#{RedmineDmsf::Webdav::AUTHENTICATION_REALM}:jsmith" + ) + token ||= Token.create!(user_id: @jsmith_user.id, action: 'dmsf_webdav_digest') + token.value = digest + assert token.save + authorization = encode_credentials(username: 'jsmith', digest: digest, target: '/dmsf/webdav') + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_authentication' => 'Digest', 'dmsf_webdav' => '1' } do + get '/dmsf/webdav', params: nil, headers: { HTTP_AUTHORIZATION: authorization } + assert_response :success + end + end + + def test_should_permit_authenticated_user + get '/dmsf/webdav', params: nil, headers: @admin + assert_response :success + end + + def test_should_include_response_headers + get '/dmsf/webdav', params: nil, headers: @admin + assert_response :success + assert_equal 'text/html', response.headers['Content-Type'] + assert response.headers['Content-Length'].to_i.positive?, + "Content-Length should be > 0, but was #{response.headers['Content-Length']}" + end + + def test_should_list_dmsf_enabled_project + get '/dmsf/webdav', params: nil, headers: @admin + assert_response :success + assert_not response.body.match(@project1.identifier).nil?, + "Expected to find project #{@project1.identifier} in return data" + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic' } do + project1_uri = Addressable::URI.encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + get '/dmsf/webdav', params: nil, headers: @admin + assert_response :success + assert_no_match @project1.identifier, response.body + assert_match project1_uri, response.body + end + end + + def test_should_not_list_non_dmsf_enabled_project + @project2.disable_module! :dmsf + get '/dmsf/webdav', params: nil, headers: @jsmith + assert_response :success + assert_not response.body.match(@project2.identifier) + end + + def test_should_return_status_404_when_project_does_not_exist + get '/dmsf/webdav/project_does_not_exist', params: nil, headers: @jsmith + assert_response :not_found + end + + def test_should_return_status_404_when_dmsf_not_enabled_for_file + get "/dmsf/webdav/#{@project2.identifier}/#{@file2.name}", params: nil, headers: @jsmith + assert_response :not_found + end + + def test_should_return_status_404_when_dmsf_not_enabled_for_folder + get "/dmsf/webdav/#{@project2.identifier}/#{@folder3.title}", params: nil, headers: @jsmith + assert_response :not_found + end + + def test_should_return_status_200_when_dmsf_not_enabled_for_project + @project2.disable_module! :dmsf + get "/dmsf/webdav/#{@project2.identifier}", params: nil, headers: @jsmith + assert_response :success + # Folders and files are not listed + assert response.body.match(@file2.name).nil? + assert response.body.match(@folder3.title).nil? + end + + def test_should_not_list_files_without_permissions + @role.remove_permission! :view_dmsf_files + get "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @jsmith + assert_response :success + # Files are not listed + assert response.body.match(@file1.name).nil? + assert response.body.match(@folder1.title) + end + + def test_should_not_list_folders_without_permissions + @role.remove_permission! :view_dmsf_folders + get "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @jsmith + assert_response :success + # Folders are not listed + assert response.body.match(@file1.name) + assert response.body.match(@folder1.title).nil? + end + + def test_download_file_from_dmsf_enabled_project + get "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @admin + assert_response :success + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic' } do + project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + get "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @admin + assert_response :conflict + get "/dmsf/webdav/#{project1_uri}/test.txt", params: nil, headers: @admin + assert_response :success + end + end + + def test_should_list_dmsf_contents_within_project + get "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @admin + assert_response :success + folder = DmsfFolder.find_by(id: 1) + assert_not_nil folder + assert response.body.match(@folder1.title), + "Expected to find #{folder.title} in return data" + file = DmsfFile.find_by(id: 1) + assert_not_nil file + assert response.body.match(file.name), + "Expected to find #{file.name} in return data" + end + + def test_user_assigned_to_project_dmsf_module_not_enabled + get "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @jsmith + assert_response :success + end + + def test_user_assigned_to_archived_project + @project1.archive + get "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @jsmith + assert_response :not_found + end + + def test_user_assigned_to_project_folder_ok + get "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @jsmith + assert_response :success + end + + def test_user_assigned_to_project_file_forbidden + @role.remove_permission! :view_dmsf_files + get "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @jsmith + assert_response :forbidden + end + + def test_user_assigned_to_project_file_ok + get "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @jsmith + assert_response :success + end + + def test_get_file_in_subproject + get "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@file12.name}", params: nil, headers: @admin + assert_response :success + end + + def test_get_folder_in_subproject + get "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@folder10.title}", params: nil, headers: @admin + assert_response :success + end +end diff --git a/test/integration/webdav/dmsf_webdav_head_test.rb b/test/integration/webdav/dmsf_webdav_head_test.rb new file mode 100644 index 00000000..35185ea1 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_head_test.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# WebDAV HEAD tests +class DmsfWebdavHeadTest < RedmineDmsf::Test::IntegrationTest + def test_head_requires_authentication + head "/dmsf/webdav/#{@project1.identifier}" + assert_response :unauthorized + check_headers_dont_exist + end + + def test_head_responds_with_authentication + head "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @admin + assert_response :success + check_headers_exist + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic' } do + head "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @admin + assert_response :not_found + project1_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + project1_uri = Addressable::URI.escape(project1_name) + head "/dmsf/webdav/#{project1_uri}", params: nil, headers: @admin + assert_response :success + end + end + + # NOTE: At present we use Rack to serve the file, this makes life easy however it removes the Etag + # header and invalidates the test - where as a folder listing will always not include a last-modified + # (but may include an etag, so there is an allowance for a 1 in 2 failure rate on (optionally) required + # headers) + def test_head_responds_to_file + head "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @admin + assert_response :success + check_headers_exist + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic' } do + head "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: @admin + assert_response :conflict + project1_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + project1_uri = Addressable::URI.escape(project1_name) + head "/dmsf/webdav/#{project1_uri}/test.txt", params: nil, headers: @admin + assert_response :success + end + end + + def test_head_responds_to_file_anonymous_other_user_agent + head "/dmsf/webdav/#{@project1.identifier}/test.txt", params: nil, headers: { HTTP_USER_AGENT: 'Other' } + assert_response :unauthorized + check_headers_dont_exist + end + + def test_head_fails_when_file_not_found + head "/dmsf/webdav/#{@project1.identifier}/not_here.txt", params: nil, headers: @admin + assert_response :not_found + check_headers_dont_exist + end + + def test_head_fails_when_file_not_found_anonymous_other_user_agent + head "/dmsf/webdav/#{@project1.identifier}/not_here.txt", params: nil, headers: { HTTP_USER_AGENT: 'Other' } + assert_response :unauthorized + check_headers_dont_exist + end + + def test_head_fails_when_folder_not_found + head '/dmsf/webdav/folder_not_here', params: nil, headers: @admin + assert_response :not_found + check_headers_dont_exist + end + + def test_head_fails_when_folder_not_found_anonymous_other_user_agent + head '/dmsf/webdav/folder_not_here', params: nil, headers: { HTTP_USER_AGENT: 'Other' } + assert_response :unauthorized + check_headers_dont_exist + end + + def test_head_fails_when_project_is_not_enabled_for_dmsf + head "/dmsf/webdav/#{@project2.identifier}/test.txt", params: nil, headers: @jsmith + assert_response :not_found + check_headers_dont_exist + end + + def test_head_file_in_subproject + head "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@file12.name}", params: nil, headers: @admin + assert_response :success + end + + def test_head_folder_in_subproject + head "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@folder10.title}", + params: nil, + headers: @admin + assert_response :success + end +end diff --git a/test/integration/webdav/dmsf_webdav_lock_test.rb b/test/integration/webdav/dmsf_webdav_lock_test.rb new file mode 100644 index 00000000..32a21ce4 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_lock_test.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) +require 'fileutils' + +# WebDAV LOCK test +class DmsfWebdavLockTest < RedmineDmsf::Test::IntegrationTest + def setup + super + @xml = %( + + + + jsmith + ) + end + + def test_lock_file_already_locked_by_other + log_user 'admin', 'admin' + process :lock, + "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", + params: @xml, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite' }) + assert_response :locked + end + + def test_lock_file + log_user 'jsmith', 'jsmith' + create_time = Time.utc(2000, 1, 2, 3, 4, 5) + refresh_time = Time.utc(2000, 1, 2, 6, 7, 8) + lock_token = nil + # Time travel, will make the usec part of the time 0 + travel_to create_time do + # Lock file + process :lock, + "/dmsf/webdav/#{@file9.project.identifier}/#{@file9.name}", + params: @xml, + headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite' }) + assert_response :success + # Verify the response + # + # + # + # + # + # infinity + # Second-604800 + # + # f5762389-6b49-4482-9a4b-ff1c8f975765 + # + # + # + # + assert_match %r{}, response.body + assert_match %r{infinity}, response.body + # 1.week = 7*24*3600=604800 seconds + assert_match %r{Second-604800}, response.body + assert_match %r{([a-z0-9\-]+)}, response.body + # Extract the locktoken, needed when refreshing the lock + response.body =~ %r{([a-z0-9\-]+)} + lock_token = Regexp.last_match(1) + # Verify the lock in the db + @file9.reload + l = @file9.lock.first + assert_equal create_time, l.created_at + assert_equal create_time, l.updated_at + assert_equal (create_time + 1.week), l.expires_at + end + travel_to refresh_time do + # Refresh lock + xml = %( + + jsmith + ) + process :lock, + "/dmsf/webdav/#{@file9.project.identifier}/#{@file9.name}", + params: xml, + headers: @jsmith.merge!( + { HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_IF: "(<#{lock_token}>)" } + ) + assert_response :success + # 1.week = 7*24*3600=604800 seconds + assert_match 'Second-604800', response.body + # Verify the lock in the db + @file9.reload + l = @file9.lock.first + assert_equal create_time, l.created_at + assert_equal refresh_time, l.updated_at + assert_equal (refresh_time + 1.week), l.expires_at + end + end + + def test_lock_file_in_subproject + log_user 'admin', 'admin' + process :lock, + "/dmsf/webdav/#{@file12.project.parent.identifier}/#{@file12.project.identifier}/#{@file12.name}", + params: @xml, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite' }) + assert_response :success + end + + def test_lock_folder_in_subproject + log_user 'admin', 'admin' + process :lock, + "/dmsf/webdav/#{@folder10.project.parent.identifier}/#{@folder10.project.identifier}/#{@folder10.title}", + params: @xml, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite' }) + assert_response :success + end + + def test_lock_subproject + log_user 'admin', 'admin' + process :lock, "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}", + params: @xml, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite' }) + assert_response :multi_status + assert_match 'HTTP/1.1 405 Method Not Allowed', response.body + end +end diff --git a/test/integration/webdav/dmsf_webdav_mkcol_test.rb b/test/integration/webdav/dmsf_webdav_mkcol_test.rb new file mode 100644 index 00000000..6ac04e14 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_mkcol_test.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# WebDAV MKCOL tests +class DmsfWebdavMkcolTest < RedmineDmsf::Test::IntegrationTest + def test_mkcol_requires_authentication + process :mkcol, '/dmsf/webdav/test1' + assert_response :unauthorized + end + + def test_mkcol_fails_to_create_folder_at_root_level + process :mkcol, '/dmsf/webdav/test1', params: nil, headers: @admin + assert_response :method_not_allowed + end + + def test_should_not_succeed_on_a_non_existant_project + process :mkcol, '/dmsf/webdav/project_doesnt_exist/test1', params: nil, headers: @admin + assert_response :conflict + end + + def test_should_not_succed_on_a_non_dmsf_enabled_project + @project1.disable_module! :dmsf + process :mkcol, "/dmsf/webdav/#{@project1.identifier}/folder", params: nil, headers: @jsmith + assert_response :not_found + end + + def test_should_not_create_folder_without_permissions + @role.remove_permission! :folder_manipulation + process :mkcol, "/dmsf/webdav/#{@project1.identifier}/folder", params: nil, headers: @jsmith + assert_response :forbidden + end + + def test_should_fail_to_create_folder_that_already_exists + process :mkcol, + "/dmsf/webdav/#{@project1.identifier}/#{@folder6.title}", + params: nil, + headers: @jsmith + assert_response :method_not_allowed + end + + def test_should_create_folder_for_non_admin_user_with_rights + process :mkcol, "/dmsf/webdav/#{@project1.identifier}/test1", params: nil, headers: @jsmith + assert_response :success + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + process :mkcol, "/dmsf/webdav/#{@project1.identifier}/test2", params: nil, headers: @jsmith + assert_response :conflict + process :mkcol, "/dmsf/webdav/#{project1_uri}/test3", params: nil, headers: @jsmith + assert_response :success # Created + end + end + + def test_create_folder_in_subproject + process :mkcol, "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/test1", + params: nil, + headers: @admin + assert_response :success + end + + def test_create_folder_with_square_brackets_of_the_same_name_as_a_sub_project + project3_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project5)) + process :mkcol, "/dmsf/webdav/#{@project1.identifier}/#{project3_uri}", + params: nil, + headers: @admin + assert_response :method_not_allowed + end + + def test_create_folder_of_the_same_name_as_a_sub_project + process :mkcol, "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}", + params: nil, + headers: @admin + assert_response :method_not_allowed + end + + def test_create_folder_with_square_brackets + folder_name = ERB::Util.url_encode('[new folder]') + process :mkcol, "/dmsf/webdav/#{@project1.identifier}/#{folder_name}", + params: nil, + headers: @admin + assert_response :conflict # Square brackets are not allowed in project names + end +end diff --git a/test/integration/webdav/dmsf_webdav_move_test.rb b/test/integration/webdav/dmsf_webdav_move_test.rb new file mode 100644 index 00000000..a9f999d6 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_move_test.rb @@ -0,0 +1,416 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) +require 'fileutils' + +# WebDAV MOVE tests +class DmsfWebdavMoveTest < RedmineDmsf::Test::IntegrationTest + def test_move_denied_for_anonymous + new_name = "#{@file1.name}.moved" + assert_no_difference '@file1.dmsf_file_revisions.count' do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + assert_response :unauthorized + end + end + + def test_move_to_new_filename_without_file_manipulation_permission + @role.remove_permission! :file_manipulation + new_name = "#{@file1.name}.moved" + assert_no_difference '@file1.dmsf_file_revisions.count' do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + ) + assert_response :forbidden + end + end + + def test_move_to_new_filename_without_file_manipulation_permission_as_admin + @role.remove_permission! :file_manipulation + new_name = "#{@file1.name}.moved" + assert_difference '@file1.dmsf_file_revisions.count', +1 do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @admin.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + ) + assert_response :created + f = DmsfFile.find_file_by_name @project1, nil, new_name + assert f, "Moved file '#{new_name}' not found in project." + end + end + + def test_without_folder_manipulation_permission + @role.remove_permission! :folder_manipulation + new_name = "#{@folder1.title}.moved" + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + ) + assert_response :forbidden + end + + def test_without_folder_manipulation_permission_as_admin + @role.remove_permission! :folder_manipulation + new_name = "#{@folder1.title}.moved" + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}", + params: nil, + headers: @admin.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + ) + assert_response :created + end + + def test_move_folder_to_another_project + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}", + params: nil, + headers: @admin.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project2.identifier}/#{@folder1.title}" } + ) + assert_response :created + @folder1.dmsf_folders.each do |d| + assert_equal @project2, d.project + end + @folder1.dmsf_files.each do |f| + assert_equal @project2, f.project + end + @folder1.dmsf_links.each do |l| + assert_equal @project2, l.project + end + end + + def test_move_non_existent_file + process :move, + "/dmsf/webdav/#{@project1.identifier}/not_a_file.txt", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/moved_file.txt" } + ) + assert_response :not_found # NotFound + end + + def test_move_to_new_filename + new_name = "#{@file1.name}.moved" + assert_difference '@file1.dmsf_file_revisions.count', +1 do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + ) + assert_response :created + f = DmsfFile.find_file_by_name @project1, nil, new_name + assert f, "Moved file '#{new_name}' not found in project." + end + end + + def test_move_to_new_filename_with_project_names + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + new_name = "#{@file1.name}.moved" + assert_difference '@file1.dmsf_file_revisions.count', +1 do + process :move, "/dmsf/webdav/#{project1_uri}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{project1_uri}/#{new_name}" } + ) + assert_response :created + f = DmsfFile.find_file_by_name @project1, nil, new_name + assert f, "Moved file '#{new_name}' not found in project." + end + end + end + + def test_move_zero_sized_to_new_filename + new_name = "#{@file10.name}.moved" + assert_no_difference '@file10.dmsf_file_revisions.count' do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@file10.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + ) + assert_response :created + f = DmsfFile.find_file_by_name @project1, nil, new_name + assert f, "Moved file '#{new_name}' not found in project." + end + end + + def test_move_to_new_folder + assert_difference '@file1.dmsf_file_revisions.count', +1 do + process( + :move, "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}/#{@file1.name}" } + ) + ) + assert_response :created + @file1.reload + assert_equal @folder1.id, @file1.dmsf_folder_id + end + end + + def test_move_to_new_folder_with_project_names + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + assert_difference '@file1.dmsf_file_revisions.count', +1 do + process :move, + "/dmsf/webdav/#{project1_uri}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{project1_uri}/#{@folder1.title}/#{@file1.name}" } + ) + assert_response :created + @file1.reload + assert_equal @folder1.id, @file1.dmsf_folder_id + end + end + end + + def test_move_zero_sized_to_new_folder + assert_no_difference '@file10.dmsf_file_revisions.count' do + process( + :move, "/dmsf/webdav/#{@project1.identifier}/#{@file10.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}/#{@file10.name}" } + ) + ) + assert_response :created + @file10.reload + assert_equal @folder1.id, @file10.dmsf_folder_id + end + end + + def test_move_to_existing_filename + assert_no_difference '@file9.dmsf_file_revisions.count' do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@file9.name}" } + ) + assert_response :precondition_failed + end + end + + def test_move_when_file_is_locked_by_other + log_user 'admin', 'admin' # login as admin + User.current = @admin_user + assert @file1.lock!, "File failed to be locked by #{User.current}" + new_name = "#{@file1.name}.moved" + assert_no_difference '@file1.dmsf_file_revisions.count' do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + ) + assert_response :locked + end + end + + def test_move_when_file_is_locked_by_other_and_user_is_admin + log_user 'jsmith', 'jsmith' # login as jsmith + User.current = @jsmith_user + assert @file1.lock!, "File failed to be locked by #{User.current}" + + new_name = "#{@file1.name}.moved" + assert_no_difference '@file1.dmsf_file_revisions.count' do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @admin.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + ) + assert_response :locked + end + end + + def test_move_when_file_is_locked_by_user + log_user 'jsmith', 'jsmith' # login as jsmith + User.current = @jsmith_user + assert @file1.lock!, "File failed to be locked by #{User.current}" + + # Move once + new_name = "#{@file1.name}.m1" + assert_difference '@file1.dmsf_file_revisions.count', +1 do + process :move, "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name}" } + ) + assert_response :success # Created + end + # Move twice, make sure that the MsOffice store sequence is not disrupting normal move + new_name2 = "#{new_name}.m2" + assert_difference '@file1.dmsf_file_revisions.count', +1 do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{new_name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{new_name2}" } + ) + assert_response :success # Created + end + end + + def test_move_msoffice_save_locked_file + # When some versions of MsOffice save a file they use the following sequence: + # 1. Save changes to a new temporary document, XXX.tmp + # 2. Rename (MOVE) document to YYY.tmp. History is lost here if the original document is moved. + # 3. Rename (MOVE) XXX.tmp to document's name. XXX.tmp must be merged to the original document otherwise the + # history is lost. + # 4. Delete YYY.tmp. + # Verify that steps 2 and 3 work. + log_user 'jsmith', 'jsmith' # login as jsmith + User.current = @jsmith_user + assert @file1.lock!, "File failed to be locked by #{User.current}" + + # First save while the file is locked, should create new revision + temp_file_name = 'AAAAAAAA.tmp' + + # Make sure that the temp-file does not exist. + temp_file = DmsfFile.find_file_by_name @project1, nil, temp_file_name + assert_not temp_file, "File '#{temp_file_name}' should not exist yet." + + # Move the original file to AAAAAAAA.tmp. The original file should not changed but a new file should be created. + assert_no_difference '@file1.dmsf_file_revisions.count' do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{temp_file_name}" } + ) + assert_response :success # Created + end + + # Verify that a new file has been created + temp_file = DmsfFile.find_file_by_name @project1, nil, temp_file_name + assert temp_file, "File '#{temp_file_name}' not found, move failed." + assert_equal temp_file.dmsf_file_revisions.count, 1 + assert_not_equal temp_file.id, @file1.id + + # Move a temporary file (use AAAAAAAA.tmp) to the original file. + assert_difference '@file1.dmsf_file_revisions.count', +1 do + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{temp_file_name}", + params: nil, + headers: @jsmith.merge!( + { + destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + 'HTTP_OVERWRITE' => 'T' + } + ) + assert_response :success # Created + end + + # Second save while file is locked, should NOT create new revision + temp_file_name = 'BBBBBBBB.tmp' + + # Make sure that the temp-file does not exist. + temp_file = DmsfFile.find_file_by_name @project1, nil, temp_file_name + assert_not temp_file, "File '#{temp_file_name}' should not exist yet." + + # Move the original file to BBBBBBBB.tmp. The original file should not change but a new file should be created. + assert_no_difference '@file1.dmsf_file_revisions.count' do + process :move, "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{temp_file_name}" } + ) + assert_response :success # Created + end + + # Verify that a new file has been created + temp_file = DmsfFile.find_file_by_name @project1, nil, temp_file_name + assert temp_file, "File '#{temp_file_name}' not found, move failed." + assert_equal temp_file.dmsf_file_revisions.count, 1 + assert_not_equal temp_file.id, @file1.id + + # Move a temporary file (use BBBBBBBB.tmp) to the original file. + assert_no_difference '@file1.dmsf_file_revisions.count' do + process :move, "/dmsf/webdav/#{@project1.identifier}/#{temp_file_name}", + params: nil, + headers: @jsmith.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@file1.name}" } + ) + assert_response :success # Created + end + end + + def test_move_file_in_subproject + dest = "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/new_file_name" + assert_difference '@file12.dmsf_file_revisions.count', +1 do + process :move, "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@file12.name}", + params: nil, + headers: @admin.merge!({ destination: dest }) + assert_response :created + end + end + + def test_move_folder_in_subproject + dest = "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/new_folder_name" + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@folder10.title}", + params: nil, + headers: @admin.merge!({ destination: dest }) + assert_response :created + @folder10.reload + assert_equal 'new_folder_name', @folder10.title + end + + def test_move_folder_in_subproject_to_the_same_name_as_subproject + dest = "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@project5.identifier}" + process :move, + "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/#{@folder10.title}", + params: nil, + headers: @admin.merge!({ destination: dest }) + assert_response :created + @folder10.reload + assert_equal @project5.identifier, @folder10.title + end + + def test_move_subproject + process :move, "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}", + params: nil, + headers: @admin.merge!( + { destination: "http://www.example.com/dmsf/webdav/#{@project1.identifier}/new_project_name" } + ) + assert_response :method_not_allowed + end +end diff --git a/test/integration/webdav/dmsf_webdav_options_test.rb b/test/integration/webdav/dmsf_webdav_options_test.rb new file mode 100644 index 00000000..7f438978 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_options_test.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# WebDAV OPTIONS tests +class DmsfWebdavOptionsTest < RedmineDmsf::Test::IntegrationTest + def test_options_requires_no_authentication_for_root_level + process :options, '/dmsf/webdav' + assert_response :success + end + + def test_options_returns_expected_allow_header_for_ro + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_strategy' => 'WEBDAV_READ_ONLY', 'dmsf_webdav' => '1' } do + process :options, '/dmsf/webdav' + assert_response :success + assert_not response.headers.blank?, 'Response headers are empty' + assert response.headers['Allow'], 'Allow header is empty or does not exist' + assert_equal response.headers['Allow'], 'OPTIONS,HEAD,GET,PROPFIND' + end + end + + def test_options_returns_expected_allow_header_for_rw + process :options, '/dmsf/webdav' + assert_response :success + assert_not response.headers.blank?, 'Response headers are empty' + assert response.headers['Allow'], 'Allow header is empty or does not exist' + assert_equal response.headers['Allow'], + 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK' + end + + def test_options_returns_expected_dav_header + process :options, '/dmsf/webdav' + assert_response :success + assert_not response.headers.blank?, 'Response headers are empty' + assert response.headers['Dav'], 'Dav header is empty or does not exist' + end + + def test_options_returns_expected_ms_auth_via_header + process :options, '/dmsf/webdav' + assert_response :success + assert_not response.headers.blank?, 'Response headers are empty' + assert response.headers['Ms-Author-Via'], 'Ms-Author-Via header is empty or does not exist' + assert response.headers['Ms-Author-Via'] == 'DAV', 'Ms-Author-Via header - expected: DAV' + end + + def test_options_requires_authentication_for_non_root_request + process :options, "/dmsf/webdav/#{@project1.identifier}" + assert_response :unauthorized + end + + def test_un_authenticated_options_returns_expected_allow_header + process :options, "/dmsf/webdav/#{@project1.identifier}" + assert_response :unauthorized + assert_not response.headers.blank?, 'Response headers are empty' + assert_nil response.headers['Allow'], 'Allow header should not exist' + end + + def test_un_authenticated_options_returns_expected_dav_header + process :options, "/dmsf/webdav/#{@project1.identifier}" + assert_response :unauthorized + assert_not response.headers.blank?, 'Response headers are empty' + assert_nil response.headers['Dav'], 'Dav header should not exist' + end + + def test_un_authenticated_options_returns_expected_ms_auth_via_header + process :options, "/dmsf/webdav/#{@project1.identifier}" + assert_response :unauthorized + assert_not response.headers.blank?, 'Response headers are empty' + assert_nil response.headers['Ms-Author-Via'], 'Ms-Author-Via header should not exist' + end + + def test_authenticated_options_returns_expected_allow_header + process :options, "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @jsmith + assert_response :success + assert_not response.headers.blank?, 'Response headers are empty' + assert response.headers['Allow'], 'Allow header is empty or does not exist' + assert_equal response.headers['Allow'], + 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK' + end + + def test_authenticated_options_returns_expected_dav_header + process :options, "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @jsmith + assert_response :success + assert_not response.headers.blank?, 'Response headers are empty' + assert response.headers['Dav'], 'Dav header is empty or does not exist' + end + + def test_authenticated_options_returns_expected_ms_auth_via_header + process :options, "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @jsmith + assert_response :success + assert_not response.headers.blank?, 'Response headers are empty' + assert response.headers['Ms-Author-Via'], 'Ms-Author-Via header is empty or does not exist' + assert response.headers['Ms-Author-Via'] == 'DAV', 'Ms-Author-Via header - expected: DAV' + end + + def test_un_authenticated_options_for_msoffice_user_agent + process :options, + "/dmsf/webdav/#{@project1.identifier}", + params: nil, + headers: { HTTP_USER_AGENT: 'Microsoft Office Word 2014' } + assert_response :unauthorized + end + + def test_authenticated_options_for_msoffice_user_agent + process :options, "/dmsf/webdav/#{@project1.identifier}", + params: nil, + headers: @admin.merge!({ HTTP_USER_AGENT: 'Microsoft Office Word 2014' }) + assert_response :success + end + + def test_un_authenticated_options_for_other_user_agent + process :options, "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: { HTTP_USER_AGENT: 'Other' } + assert_response :unauthorized + end + + def test_authenticated_options_for_other_user_agent + process :options, + "/dmsf/webdav/#{@project1.identifier}", + params: nil, + headers: @admin.merge!({ HTTP_USER_AGENT: 'Other' }) + assert_response :success + + project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + process :options, "/dmsf/webdav/#{project1_uri}", params: nil, headers: @admin.merge!({ HTTP_USER_AGENT: 'Other' }) + assert_response :success + end + + def test_options_for_subproject + process :options, "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}", params: nil, headers: @admin + assert_response :success + end +end diff --git a/test/integration/webdav/dmsf_webdav_post_test.rb b/test/integration/webdav/dmsf_webdav_post_test.rb new file mode 100644 index 00000000..0c00fbc5 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_post_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# WebDAV POST tests +class DmsfWebdavPostTest < RedmineDmsf::Test::IntegrationTest + def test_post_request_authenticated + post '/dmsf/webdav/' + assert_response :unauthorized + end + + # Test post is not implemented + def test_post_not_implemented + post '/dmsf/webdav/', params: nil, headers: @admin + assert_response :not_implemented + end +end diff --git a/test/integration/webdav/dmsf_webdav_propfind_test.rb b/test/integration/webdav/dmsf_webdav_propfind_test.rb new file mode 100644 index 00000000..5e5ceb30 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_propfind_test.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) +require 'uri' + +# WebDAV PROPFIND tests +class DmsfWebdavPropfindTest < RedmineDmsf::Test::IntegrationTest + def test_propfind_denied_for_anonymous + process :propfind, '/dmsf/webdav/', params: nil, headers: @anonymous.merge!({ HTTP_DEPTH: '0' }) + assert_response :unauthorized + end + + def test_propfind_depth0_on_root_for_user + process :propfind, '/dmsf/webdav/', params: nil, headers: @jsmith.merge!({ HTTP_DEPTH: '0' }) + assert_response :multi_status + assert response.body.include?('http://www.example.com/dmsf/webdav/') + assert response.body.include?('/') + end + + def test_propfind_depth1_on_root_for_user + process :propfind, '/dmsf/webdav/', params: nil, headers: @someone.merge!({ HTTP_DEPTH: '1' }) + assert_response :multi_status + assert response.body.include?('http://www.example.com/dmsf/webdav/') + assert response.body.include?('/') + end + + def test_propfind_depth0_on_root_for_admin + process :propfind, '/dmsf/webdav/', params: nil, headers: @admin.merge!({ HTTP_DEPTH: '0' }) + assert_response :multi_status + assert response.body.include?('http://www.example.com/dmsf/webdav/') + assert response.body.include?('/') + end + + def test_propfind_depth1_on_root_for_admin_with_project_names + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic' } do + process :propfind, '/dmsf/webdav/', params: nil, headers: @admin.merge!({ HTTP_DEPTH: '1' }) + assert_response :multi_status + assert response.body.include?('http://www.example.com/dmsf/webdav/') + assert response.body.include?('/') + # project.identifier should not match when using project names + assert_not response.body.include?( + "http://www.example.com/dmsf/webdav/#{@project1.identifier}/" + ) + assert_not response.body.include?("#{@project1.identifier}") + # but the project name should match + project1_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + project1_uri = Addressable::URI.escape(project1_name) + assert response.body.include?("http://www.example.com/dmsf/webdav/#{project1_uri}/") + assert response.body.include?("#{project1_name}") + end + end + + def test_propfind_depth0_on_project1_for_non_member + process :propfind, + "/dmsf/webdav/#{@project1.identifier}", + params: nil, + headers: @someone.merge!({ HTTP_DEPTH: '0' }) + assert_response :success + end + + def test_propfind_depth0_on_folder1_for_non_member + process :propfind, "/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}", + params: nil, + headers: @someone.merge!({ HTTP_DEPTH: '0' }) + assert_response :not_found + end + + def test_propfind_depth0_on_file1_for_non_member + process :propfind, "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: nil, + headers: @someone.merge!({ HTTP_DEPTH: '0' }) + assert_response :not_found + end + + def test_propfind_depth0_on_project1_for_admin + process :propfind, "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @admin.merge!({ HTTP_DEPTH: '0' }) + assert_response :multi_status + assert response.body.include?("http://www.example.com/dmsf/webdav/#{@project1.identifier}/") + assert response.body.include?( + "#{RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)}" + ) + end + + def test_propfind_depth0_on_project1_for_admin_with_project_names + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic' } do + process :propfind, + "/dmsf/webdav/#{@project1.identifier}", + params: nil, + headers: @admin.merge!({ HTTP_DEPTH: '0' }) + assert_response :not_found + project1_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + project1_uri = Addressable::URI.escape(project1_name) + process :propfind, "/dmsf/webdav/#{project1_uri}", params: nil, headers: @admin.merge!({ HTTP_DEPTH: '0' }) + assert_response :multi_status + assert response.body.include?("http://www.example.com/dmsf/webdav/#{project1_uri}/") + project1_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + assert response.body.include?("#{project1_name}") + end + end + + def test_propfind_depth1_on_project1_for_admin + process :propfind, "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @admin.merge!({ HTTP_DEPTH: '1' }) + assert_response :multi_status + # Project + assert response.body.include?("http://www.example.com/dmsf/webdav/#{@project1.identifier}/") + assert response.body.include?( + "#{RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)}" + ) + # Folders + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@folder1.title}/" + ) + assert response.body.include?("#{@folder1.title}") + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@folder6.title}/" + ) + assert response.body.include?("#{@folder6.title}") + # Files + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@file1.name}" + ) + assert response.body.include?("#{@file1.name}") + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@file9.name}" + ) + assert response.body.include?("#{@file9.name}") + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@file10.name}" + ) + assert response.body.include?("#{@file10.name}") + end + + def test_propfind_depth1_on_project1_for_admin_with_project_names + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic' } do + process :propfind, + "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: @admin.merge!({ HTTP_DEPTH: '1' }) + assert_response :not_found + project1_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + project1_uri = Addressable::URI.escape(project1_name) + process :propfind, "/dmsf/webdav/#{project1_uri}", params: nil, headers: @admin.merge!({ HTTP_DEPTH: '1' }) + assert_response :multi_status + # Project + project1_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + project1_uri = Addressable::URI.escape(project1_name) + assert response.body.include?("http://www.example.com/dmsf/webdav/#{project1_uri}/") + # Folders + project1_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + project1_uri = Addressable::URI.escape(project1_name) + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{project1_uri}/#{@folder1.title}/" + ) + assert response.body.include?("#{@folder1.title}") + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{project1_uri}/#{@folder6.title}/" + ) + assert response.body.include?("#{@folder6.title}") + # Files + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{project1_uri}/#{@file1.name}" + ) + assert response.body.include?("#{@file1.name}") + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{project1_uri}/#{@file9.name}" + ) + assert response.body.include?("#{@file9.name}") + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{project1_uri}/#{@file10.name}" + ) + assert response.body.include?("#{@file10.name}") + end + end + + def test_propfind_depth1_on_root_for_admin + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic' } do + project1_new_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + project1_new_uri = ERB::Util.url_encode(project1_new_name) + process :propfind, "/dmsf/webdav/#{project1_new_uri}", params: nil, headers: @admin.merge!({ HTTP_DEPTH: '1' }) + assert_response :multi_status + assert response.body.include?("http://www.example.com/dmsf/webdav/#{project1_new_uri}/") + assert response.body.include?("#{project1_new_name}") + end + end + + def test_propfind_for_subproject + process :propfind, "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}", + params: nil, + headers: @admin.merge!({ HTTP_DEPTH: '1' }) + assert_response :multi_status + assert response.body.include?( + "http://www.example.com/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/" + ) + assert response.body.include?( + "#{RedmineDmsf::Webdav::ProjectResource.create_project_name(@project5)}" + ) + end +end diff --git a/test/integration/webdav/dmsf_webdav_put_test.rb b/test/integration/webdav/dmsf_webdav_put_test.rb new file mode 100644 index 00000000..34834ab3 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_put_test.rb @@ -0,0 +1,397 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) +require 'fileutils' + +# WebDAV PUT tests +class DmsfWebdavPutTest < RedmineDmsf::Test::IntegrationTest + def setup + super + @cv22 = CustomValue.find(22) + end + + def test_put_denied_unless_authenticated_root + put '/dmsf/webdav' + assert_response :unauthorized + end + + def test_put_denied_unless_authenticated + put "/dmsf/webdav/#{@project1.identifier}" + assert_response :unauthorized + end + + def test_put_denied_with_failed_authentication_root + put '/dmsf/webdav', params: nil, headers: credentials('admin', 'badpassword') + assert_response :unauthorized + end + + def test_put_denied_with_failed_authentication + put "/dmsf/webdav/#{@project1.identifier}", params: nil, headers: credentials('admin', 'badpassword') + assert_response :unauthorized + end + + def test_put_denied_at_root_level + put '/dmsf/webdav/test.txt', params: '1234', headers: @admin.merge!({ content_type: :text }) + assert_response :forbidden + end + + def test_put_denied_on_folder + put "/dmsf/webdav/#{@project1.identifier}", params: '1234', headers: @admin.merge!({ content_type: :text }) + assert_response :forbidden + end + + def test_put_failed_on_non_existant_project + put '/dmsf/webdav/not_a_project/file.txt', params: '1234', headers: @admin.merge!({ content_type: :text }) + assert_response :conflict # not_a_project does not exist - file.txt cannot be created + end + + def test_put_as_admin_granted_on_dmsf_enabled_project + put "/dmsf/webdav/#{@project1.identifier}/test-1234.txt", + params: '1234', + headers: @admin.merge!({ content_type: :text }) + assert_response :created + # Lets check for our file + file = DmsfFile.find_file_by_name @project1, nil, 'test-1234.txt' + assert file, 'Check for files existance' + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + put "/dmsf/webdav/#{@project1.identifier}/test-1234.txt", + params: '1234', + headers: @admin.merge!({ content_type: :text }) + assert_response :conflict + put "/dmsf/webdav/#{project1_uri}/test-1234.txt", params: '1234', headers: @admin.merge!({ content_type: :text }) + assert_response :created + end + end + + def test_put_failed_as_jsmith_on_non_dmsf_enabled_project + @project2.disable_module! :dmsf + put "/dmsf/webdav/#{@project2.identifier}/test-1234.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :forbidden + # Lets check for our file + file = DmsfFile.find_file_by_name @project2, nil, 'test-1234.txt' + assert_nil file, 'Check for files existance' + end + + def test_put_failed_when_no_permission + @role.remove_permission! :file_manipulation + put "/dmsf/webdav/#{@project1.identifier}/test-1234.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :forbidden + end + + def test_put_succeeds_for_non_admin_with_correct_permissions + put "/dmsf/webdav/#{@project1.identifier}/test-1234.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + # Lets check for our file + file = DmsfFile.find_file_by_name @project1, nil, 'test-1234.txt' + assert file, 'File test-1234 was not found in projects dmsf folder.' + assert file.last_revision + assert_equal 'SHA256', file.last_revision.digest_type + + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + put "/dmsf/webdav/#{@project1.identifier}/test-1234.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :conflict + + project1_uri = ERB::Util.url_encode(RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1)) + put "/dmsf/webdav/#{project1_uri}/test-1234.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created # Now we have permissions + end + end + + def test_put_writes_revision_successfully_for_unlocked_file + file = DmsfFile.find_file_by_name @project1, nil, 'test.txt' + assert_not_nil file, 'test.txt file not found' + assert_difference 'file.dmsf_file_revisions.count', +1 do + put "/dmsf/webdav/#{@project1.identifier}/test.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + end + + def test_put_fails_revision_when_file_is_locked + log_user 'admin', 'admin' # login as admin + file = DmsfFile.find_file_by_name @project1, nil, 'test.txt' + assert file.lock!, "File failed to be locked by #{User.current}" + assert_no_difference 'file.dmsf_file_revisions.count' do + put "/dmsf/webdav/#{@project1.identifier}/test.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :locked + end + end + + def test_put_fails_revision_when_file_is_locked_and_user_is_administrator + log_user 'jsmith', 'jsmith' # login as jsmith + file = DmsfFile.find_file_by_name @project1, nil, 'test.txt' + assert file.lock!, "File failed to be locked by #{User.current}" + assert_no_difference 'file.dmsf_file_revisions.count' do + put "/dmsf/webdav/#{@project1.identifier}/test.txt", + params: '1234', + headers: @admin.merge!({ content_type: :text }) + assert_response :locked + end + end + + def test_put_accepts_revision_when_file_is_locked_and_user_is_same_as_lock_holder + # Lock the file + User.current = @jsmith_user + file = DmsfFile.find_file_by_name @project1, nil, 'test.txt' + l = file.lock! + assert l, "File failed to be locked by #{User.current}" + assert_equal file.last_revision.id, l.dmsf_file_last_revision_id + + # First PUT should always create new revision. + User.current = @jsmith_user + assert_difference 'file.dmsf_file_revisions.count', +1 do + put "/dmsf/webdav/#{@project1.identifier}/test.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + + # Second PUT on a locked file should only update the revision that were created on the first PUT + User.current = @jsmith_user + assert_no_difference 'file.dmsf_file_revisions.count' do + put "/dmsf/webdav/#{@project1.identifier}/test.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + + # Unlock + User.current = @jsmith_user + assert file.unlock!, "File failed to be unlocked by #{User.current}" + + # Lock file again, but this time delete the revision that were stored in the lock + User.current = @jsmith_user + file = DmsfFile.find_file_by_name @project1, nil, 'test.txt' + l = file.lock! + assert l, "File failed to be locked by #{User.current}" + assert_equal file.last_revision.id, l.dmsf_file_last_revision_id + + # Delete the last revision, the revision that were stored in the lock. + file.last_revision.delete commit: true + + # First PUT should always create new revision. + User.current = @jsmith_user + assert_difference 'file.dmsf_file_revisions.count', +1 do + put "/dmsf/webdav/#{@project1.identifier}/test.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + + # Second PUT on a locked file should only update the revision that were created on the first PUT + User.current = @jsmith_user + assert_no_difference 'file.dmsf_file_revisions.count' do + put "/dmsf/webdav/#{@project1.identifier}/test.txt", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + end + + def test_put_ignored_files_default + # Ignored patterns: /^(\._|\.DS_Store$|Thumbs.db$)/ + put "/dmsf/webdav/#{@project1.identifier}/._test.txt", + params: '1234', + headers: @admin.merge!({ content_type: :text }) + assert_response :no_content + put "/dmsf/webdav/#{@project1.identifier}/.DS_Store", + params: '1234', + headers: @admin.merge!({ content_type: :text }) + assert_response :no_content + put "/dmsf/webdav/#{@project1.identifier}/Thumbs.db", + params: '1234', + headers: @admin.merge!({ content_type: :text }) + assert_response :no_content + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => nil, + 'dmsf_webdav_ignore' => '.dump$', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + put "/dmsf/webdav/#{@project1.identifier}/test.dump", + params: '1234', + headers: @admin.merge!({ content_type: :text }) + assert_response :no_content + end + end + + def test_put_non_versioned_files + credentials = @admin.merge!({ content_type: :text }) + + put "/dmsf/webdav/#{@project1.identifier}/file1.tmp", params: '1234', headers: credentials + assert_response :success + file1 = DmsfFile.find_by(project_id: @project1.id, dmsf_folder: nil, name: 'file1.tmp') + assert file1 + assert_difference 'file1.dmsf_file_revisions.count', 0 do + put "/dmsf/webdav/#{@project1.identifier}/file1.tmp", params: '5678', headers: credentials + assert_response :created + end + assert_difference 'file1.dmsf_file_revisions.count', 0 do + put "/dmsf/webdav/#{@project1.identifier}/file1.tmp", params: '9ABC', headers: credentials + assert_response :created + end + + put "/dmsf/webdav/#{@project1.identifier}/~$file2.txt", params: '1234', headers: credentials + assert_response :success + file2 = DmsfFile.find_by(project_id: @project1.id, dmsf_folder_id: nil, name: '~$file2.txt') + assert file2 + assert_difference 'file2.dmsf_file_revisions.count', 0 do + put "/dmsf/webdav/#{@project1.identifier}/~$file2.txt", params: '5678', headers: credentials + assert_response :created + end + assert_difference 'file2.dmsf_file_revisions.count', 0 do + put "/dmsf/webdav/#{@project1.identifier}/~$file2.txt", params: '9ABC', headers: credentials + assert_response :created + end + + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => nil, + 'dmsf_webdav_disable_versioning' => '.dump$', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + put "/dmsf/webdav/#{@project1.identifier}/file3.dump", params: '1234', headers: credentials + assert_response :success + file3 = DmsfFile.find_by(project_id: @project1.id, dmsf_folder_id: nil, name: 'file3.dump') + assert file3 + assert_difference 'file3.dmsf_file_revisions.count', 0 do + put "/dmsf/webdav/#{@project1.identifier}/file3.dump", params: '5678', headers: credentials + assert_response :created + end + assert_difference 'file3.dmsf_file_revisions.count', 0 do + put "/dmsf/webdav/#{@project1.identifier}/file3.dump", params: '9ABC', headers: credentials + assert_response :created + end + end + end + + def test_put_into_subproject + put "/dmsf/webdav/#{@project1.identifier}/#{@project5.identifier}/test-1234.txt", + params: '1234', + headers: @admin.merge!({ content_type: :text }) + assert_response :created + assert DmsfFile.find_by(project_id: @project5.id, dmsf_folder: nil, name: 'test-1234.txt') + end + + def test_put_keep_title + @file1.last_revision.title = 'Keep that title' + assert @file1.last_revision.save + assert_difference '@file1.dmsf_file_revisions.count', +1 do + put "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + @file1.last_revision.reload + assert_equal @file1.last_revision.title, 'Keep that title' + end + + def test_put_keep_custom_field_values + @file1.last_revision.custom_values << @cv22 + assert @file1.last_revision.save + assert_difference '@file1.dmsf_file_revisions.count', +1 do + put "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + @file1.last_revision.reload + assert_equal @file1.last_revision.custom_values.first.value, @cv22.value + end + + def test_ignore_1b_files_on + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => nil, + 'dmsf_webdav_ignore_1b_file_for_authentication' => '1', + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + put "/dmsf/webdav/#{@project1.identifier}/1bfile.txt", + params: '1', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :no_content + end + end + + def test_ignore_1b_files_off + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => nil, + 'dmsf_webdav_ignore_1b_file_for_authentication' => nil, + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + put "/dmsf/webdav/#{@project1.identifier}/1bfile.txt", + params: '1', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + end + + def test_files_exceeded_max_attachment_size + with_settings attachment_max_size: '1', + plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => nil, + 'dmsf_webdav' => '1', + 'dmsf_webdav_authentication' => 'Basic', + 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do + file_content = 'x' * 2.kilobytes + put "/dmsf/webdav/#{@project1.identifier}/2kbfile.txt", + params: file_content, + headers: @jsmith.merge!({ content_type: :text }) + assert_response :unprocessable_entity + end + end + + def test_put_digest + assert_difference '@file1.dmsf_file_revisions.count', +1 do + put "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + sha = Digest::SHA256.file(@file1.last_revision.disk_file) + assert_equal sha, @file1.last_revision.digest + end + + def test_put_version + assert_difference '@file1.dmsf_file_revisions.count', +1 do + put "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", + params: '1234', + headers: @jsmith.merge!({ content_type: :text }) + assert_response :created + end + assert_equal '1.2', @file1.last_revision.version + end +end diff --git a/test/integration/webdav/dmsf_webdav_unlock_test.rb b/test/integration/webdav/dmsf_webdav_unlock_test.rb new file mode 100644 index 00000000..d8ab4564 --- /dev/null +++ b/test/integration/webdav/dmsf_webdav_unlock_test.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) +require 'fileutils' + +# WebDAV UNLOCK tests +class DmsfWebdavUnlockTest < RedmineDmsf::Test::IntegrationTest + def test_unlock_file + log_user 'admin', 'admin' + l = @file2.locks.first + process :unlock, + "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", + params: nil, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :success + end + + def test_unlock_file_locked_by_someone_else + log_user 'jsmith', 'jsmith' + l = @file2.locks.first + process :unlock, + "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", + params: nil, + headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :forbidden + end + + def test_unlock_file_with_invalid_token + log_user 'admin', 'admin' + process :unlock, "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", + params: nil, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', + HTTP_LOCK_TOKEN: 'invalid_token' }) + assert_response :bad_request + end + + def test_unlock_file_not_locked + log_user 'admin', 'admin' + l = @file2.locks.first + process :unlock, + "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", + params: nil, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :success + process :unlock, + "/dmsf/webdav/#{@file2.project.identifier}/#{@file2.name}", + params: nil, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :no_content + end + + def test_unlock_folder_wrong_path + log_user 'jsmith', 'jsmith' + l = @folder2.locks.first + # folder1 is missing in the path + process :unlock, + "/dmsf/webdav/#{@folder2.project.identifier}/#{@folder2.title}", + params: nil, + headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :forbidden + end + + def test_unlock_folder + log_user 'jsmith', 'jsmith' + l = @folder2.locks.first + process :unlock, "/dmsf/webdav/#{@folder2.project.identifier}/#{@folder2.dmsf_folder.title}/#{@folder2.title}", + params: nil, + headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :success + end + + def test_unlock_folder_not_locked + log_user 'jsmith', 'jsmith' # login as jsmith + l = @folder2.locks.first + process :unlock, "/dmsf/webdav/#{@folder2.project.identifier}/#{@folder2.dmsf_folder.title}/#{@folder2.title}", + params: nil, + headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :success + process :unlock, "/dmsf/webdav/#{@folder2.project.identifier}/#{@folder2.dmsf_folder.title}/#{@folder2.title}", + params: nil, + headers: @jsmith.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :no_content + end + + def test_unlock_file_in_subproject + log_user 'admin', 'admin' # login as admin + User.current = @admin_user + l = @file12.lock! + assert l, "File failed to be locked by #{User.current}" + process :unlock, "/dmsf/webdav/#{@file12.project.parent.identifier}/#{@file12.project.identifier}/#{@file12.name}", + params: nil, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :success + end + + def test_unlock_folder_in_subproject + log_user 'admin', 'admin' # login as admin + User.current = @admin_user + l = @folder10.lock! + assert l, "Folder failed to be locked by #{User.current}" + process :unlock, + "/dmsf/webdav/#{@folder10.project.parent.identifier}/#{@folder10.project.identifier}/#{@folder10.title}", + params: nil, + headers: @admin.merge!({ HTTP_DEPTH: 'infinity', HTTP_TIMEOUT: 'Infinite', HTTP_LOCK_TOKEN: l.uuid }) + assert_response :success + end +end diff --git a/test/integration_test.rb b/test/integration_test.rb new file mode 100644 index 00000000..4b993a9a --- /dev/null +++ b/test/integration_test.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Test + # Integration test + class IntegrationTest < Redmine::IntegrationTest + def initialize(name) + super + # Load all plugin's fixtures + dir = File.join(File.dirname(__FILE__), 'fixtures') + ext = '.yml' + Dir.glob("#{dir}/**/*#{ext}").each do |file| + fixture = File.basename(file, ext) + ActiveRecord::FixtureSet.create_fixtures dir, fixture + end + end + + def setup + @admin = credentials('admin', 'admin') + @admin_user = User.find_by(login: 'admin') + @jsmith = credentials('jsmith', 'jsmith') + @jsmith_user = User.find_by(login: 'jsmith') + @someone = credentials('someone', 'foo') + @anonymous = credentials('') + @project1 = Project.find 1 + @project2 = Project.find 2 + @project5 = Project.find 5 + [@project1, @project2, @project5].each do |project| + project.enable_module! :dmsf + end + @file1 = DmsfFile.find 1 + @file2 = DmsfFile.find 2 + @file9 = DmsfFile.find 9 + @file10 = DmsfFile.find 10 + @file12 = DmsfFile.find 12 + @folder1 = DmsfFolder.find 1 + @folder2 = DmsfFolder.find 2 + @folder3 = DmsfFolder.find 3 + @folder6 = DmsfFolder.find 6 + @folder7 = DmsfFolder.find 7 + @folder10 = DmsfFolder.find 10 + @folder_link1 = DmsfLink.find 1 + @role = Role.find_by(name: 'Manager') + @role.add_permission! :view_dmsf_folders + @role.add_permission! :folder_manipulation + @role.add_permission! :view_dmsf_files + @role.add_permission! :file_manipulation + @role.add_permission! :file_delete + Setting.plugin_redmine_dmsf['dmsf_webdav'] = '1' + Setting.plugin_redmine_dmsf['dmsf_webdav_strategy'] = 'WEBDAV_READ_WRITE' + Setting.plugin_redmine_dmsf['dmsf_webdav_use_project_names'] = '0' + Setting.plugin_redmine_dmsf['dmsf_projects_as_subfolders'] = '0' + Setting.plugin_redmine_dmsf['dmsf_storage_directory'] = File.join('files', ['dmsf']) + Setting.plugin_redmine_dmsf['dmsf_webdav_authentication'] = 'Basic' + FileUtils.cp_r File.join(File.expand_path('../fixtures/files', __FILE__), '.'), DmsfFile.storage_path + User.current = nil + end + + def teardown + # Delete our tmp folder + FileUtils.rm_rf DmsfFile.storage_path + rescue StandardError => e + Rails.logger.error e.message + end + + protected + + def check_headers_exist + assert_not response.headers.blank?, 'Head returned without headers' # Headers exist? + values = {} + values[:etag] = { optional: true, content: response.headers['Etag'] } + values[:content_type] = response.headers['Content-Type'] + values[:last_modified] = { optional: true, content: response.headers['Last-Modified'] } + single_optional = false + values.each do |key, val| + if val.is_a?(Hash) + if val[:optional].nil? || !val[:optional] + assert_not val[:content].blank?, "Expected header #{key} was empty." if single_optional + else + single_optional = true + end + else + assert_not val.blank?, "Expected header #{key} was empty." + end + end + end + + def check_headers_dont_exist + assert_not response.headers.blank?, 'Head returned without headers' # Headers exist? + values = {} + values[:etag] = response.headers['Etag'] + values[:last_modified] = response.headers['Last-Modified'] + values.each do |key, val| + assert val.blank?, "Expected header #{key} should be empty." + end + end + + def encode_credentials(options) + options.reverse_merge!(nc: '00000001', cnonce: '0a4f113b', password_is_ha1: false) + # Perform unauthenticated request to retrieve digest parameters to use on subsequent request + target = options.delete(:target) || :index + get target + assert_response :unauthorized + # Credentials + credentials = { + uri: target, + realm: RedmineDmsf::Webdav::AUTHENTICATION_REALM, + username: options[:username], + nonce: ActionController::HttpAuthentication::Digest.nonce(Rails.configuration.secret_key_base), + opaque: ActionController::HttpAuthentication::Digest.opaque(Rails.configuration.secret_key_base) + } + credentials.merge!(options) + path_info = @request.env['PATH_INFO'].to_s + uri = options[:uri] || path_info + credentials[uri] = uri + @request.env['ORIGINAL_FULLPATH'] = path_info + ha2 = ActiveSupport::Digest.hexdigest("GET:#{target}") + nonce = ActionController::HttpAuthentication::Digest.nonce(Rails.configuration.secret_key_base) + ha1 = options.delete(:digest) + credentials[:response] = ActiveSupport::Digest.hexdigest("#{ha1}:#{nonce}:#{ha2}") + "Digest #{credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}=#{v[1]}" }.join(',')}" + end + end + end +end diff --git a/test/test_case.rb b/test/test_case.rb new file mode 100644 index 00000000..32fb5fef --- /dev/null +++ b/test/test_case.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Test + # Test case + class TestCase < ActionDispatch::IntegrationTest + def initialize(name) + super + # Load all plugin's fixtures + dir = File.join(File.dirname(__FILE__), 'fixtures') + ext = '.yml' + Dir.glob("#{dir}/**/*#{ext}").each do |file| + fixture = File.basename(file, ext) + ActiveRecord::FixtureSet.create_fixtures dir, fixture + end + end + + def setup + @admin = User.find_by(login: 'admin') + @jsmith = User.find_by(login: 'jsmith') + @dlopper = User.find_by(login: 'dlopper') + @someone = User.find_by(login: 'someone') + @project1 = Project.find 1 + with_settings plugin_redmine_dmsf: { 'dmsf_webdav_use_project_names' => '1' } do + @project1_name = RedmineDmsf::Webdav::ProjectResource.create_project_name(@project1) + end + @project1_uri = Addressable::URI.escape(@project1_name) + @project2 = Project.find 2 + @project5 = Project.find 5 + [@project1, @project2, @project5].each do |project| + project.enable_module! :dmsf + project.enable_module! :issue_tracking + end + @file1 = DmsfFile.find 1 + @file2 = DmsfFile.find 2 + @file4 = DmsfFile.find 4 + @file6 = DmsfFile.find 6 + @file9 = DmsfFile.find 9 + @file10 = DmsfFile.find 10 + @file12 = DmsfFile.find 12 + @file13 = DmsfFile.find 13 + @folder1 = DmsfFolder.find 1 + @folder2 = DmsfFolder.find 2 + @folder3 = DmsfFolder.find 3 + @folder4 = DmsfFolder.find 4 + @folder5 = DmsfFolder.find 5 + @folder6 = DmsfFolder.find 6 + @folder7 = DmsfFolder.find 7 + @role_manager = Role.find_by(name: 'Manager') + @role_developer = Role.find_by(name: 'Developer') + [@role_manager, @role_developer].each do |role| + role.add_permission! :view_dmsf_folders + role.add_permission! :folder_manipulation + role.add_permission! :view_dmsf_files + role.add_permission! :file_manipulation + role.add_permission! :file_delete + role.add_permission! :email_documents + role.add_permission! :manage_workflows + role.add_permission! :file_approval + end + Setting.plugin_redmine_dmsf['dmsf_storage_directory'] = File.join('files', ['dmsf']) + Setting.plugin_redmine_dmsf['dmsf_projects_as_subfolders'] = nil + Setting.text_formatting = 'Textile' + FileUtils.cp_r File.join(File.expand_path('../fixtures/files', __FILE__), '.'), DmsfFile.storage_path + User.current = nil + end + + def teardown + # Delete our tmp folder + FileUtils.rm_rf DmsfFile.storage_path + rescue StandardError => e + Rails.logger.error e.message + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..f68422de --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +# Load the normal Rails helper +require File.expand_path('../../../../test/test_helper', __FILE__) + +require_relative 'test_case' +require_relative 'integration_test' +require_relative 'unit_test' +require_relative 'helper_test' diff --git a/test/unit/custom_field_dmsf_file_format_test.rb b/test/unit/custom_field_dmsf_file_format_test.rb new file mode 100644 index 00000000..a02ff337 --- /dev/null +++ b/test/unit/custom_field_dmsf_file_format_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# File revision tests +class CustomFieldDmsfFileFormatTest < RedmineDmsf::Test::UnitTest + def setup + super + User.current = @jsmith + @issue = Issue.find 1 + @field = IssueCustomField.create!(name: 'DMS Document rev.', field_format: 'dmsf_file_revision') + end + + def test_possible_values_options + n = @issue.project.dmsf_files.visible.all.size + DmsfFolder.visible(false).where(project_id: @issue.project.id).find_each do |f| + n += f.dmsf_files.visible.all.size + end + assert_equal n, @field.possible_values_options(@issue).size + end + + def test_edit_tag_when_member_not_found + User.current = User.generate! + view = ActionView::Base.new(ActionController::Base.view_paths, {}, ActionController::Base.new) + view.extend(ApplicationHelper) + + begin + @field.format.edit_tag(view, + "issue_custom_field_values_#{@field.id}", + "issue[custom_field_values][#{@field.id}]", + CustomValue.create!(custom_field: @field, customized: @issue)) + assert true + rescue NoMethodError => e + flunk "Test failure: #{e.message}" + end + end +end diff --git a/test/unit/custom_field_path_test.rb b/test/unit/custom_field_path_test.rb new file mode 100644 index 00000000..7f689727 --- /dev/null +++ b/test/unit/custom_field_path_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# CustomField patch test +class CustomFieldTest < RedmineDmsf::Test::UnitTest + def test_should_have_dmsf_not_inheritable_listed_as_safe_attribute + assert CustomField.new.safe_attribute_names.include?('dmsf_not_inheritable') + end +end diff --git a/test/unit/dmsf_file_revision_test.rb b/test/unit/dmsf_file_revision_test.rb new file mode 100644 index 00000000..1f8eba09 --- /dev/null +++ b/test/unit/dmsf_file_revision_test.rb @@ -0,0 +1,340 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# File revision tests +class DmsfFileRevisionTest < RedmineDmsf::Test::UnitTest + include Redmine::I18n + + def setup + super + @revision1 = DmsfFileRevision.find 1 + @revision2 = DmsfFileRevision.find 2 + @revision3 = DmsfFileRevision.find 3 + @revision5 = DmsfFileRevision.find 5 + @revision8 = DmsfFileRevision.find 8 + @wf1 = DmsfWorkflow.find 1 + end + + def test_file_title_length_validation + file = DmsfFileRevision.new(title: Array.new(256).map { 'a' }.join, + name: 'Test Revision', + major_version: 1) + assert file.invalid? + assert_equal ['Title is too long (maximum is 255 characters)'], file.errors.full_messages + end + + def test_file_name_length_validation + file = DmsfFileRevision.new(name: Array.new(256).map { 'a' }.join, + title: 'Test Revision', + major_version: 1) + assert file.invalid? + assert_equal ['Name is too long (maximum is 255 characters)'], file.errors.full_messages + end + + def test_file_disk_filename_length_validation + file = DmsfFileRevision.new(disk_filename: Array.new(256).map { 'a' }.join, + title: 'Test Revision', + name: 'Test Revision', + major_version: 1) + assert file.invalid? + assert_equal ['Disk filename is too long (maximum is 255 characters)'], file.errors.full_messages + end + + def test_delete_restore + @revision5.delete commit: false + assert @revision5.deleted?, "File revision #{@revision5.name} hasn't been deleted" + @revision5.restore + assert_not @revision5.deleted?, "File revision #{@revision5.name} hasn't been restored" + end + + def test_destroy + @revision5.delete commit: true + assert_nil DmsfFileRevision.find_by(id: @revision5.id) + end + + def test_digest_type + # Old type MD5 + assert_equal 'MD5', @revision1.digest_type + end + + def test_new_storage_filename + # Create a file. + f = DmsfFile.new + f.project_id = 1 + f.name = 'Testfile.txt' + f.dmsf_folder = nil + f.notification = RedmineDmsf.dmsf_default_notifications? + f.save + + # Create two new revisions, r1 and r2 + r1 = DmsfFileRevision.new + r1.minor_version = 0 + r1.major_version = 1 + r1.dmsf_file = f + r1.user = User.current + r1.name = 'Testfile.txt' + r1.title = DmsfFileRevision.filename_to_title(r1.name) + r1.description = nil + r1.comment = nil + r1.mime_type = nil + r1.size = 4 + + r2 = r1.clone + r2.minor_version = 1 + + assert r1.valid? + assert r2.valid? + + # This is a very stupid since the generation and storing of files below must be done during the + # same second, so wait until the microsecond part of the DateTime is less than 10 ms, should be + # plenty of time to do the rest then. + wait_timeout = 2_000 + while DateTime.current.usec > 10_000 + wait_timeout -= 10 + flunk 'Waited too long.' if wait_timeout <= 0 + sleep 0.01 + end + + # First, generate the r1 storage filename and save the file + r1.disk_filename = r1.new_storage_filename + assert r1.save + # Just make sure the file exists + File.binwrite r1.disk_file, '1234' + + # Directly after the file has been stored generate the r2 storage filename. + # Hopefully the seconds part of the DateTime.current has not changed and the generated filename will + # be on the same second but it should then be increased by 1. + r2.disk_filename = r2.new_storage_filename + assert_not_equal r1.disk_filename, r2.disk_filename, 'The disk filename should not be equal for two revisions.' + end + + def test_invalid_filename_extension + with_settings(attachment_extensions_allowed: 'txt') do + r1 = DmsfFileRevision.new + r1.minor_version = 0 + r1.major_version = 1 + r1.dmsf_file = @file1 # name test.txt + r1.user = User.current + r1.name = 'test.txt.png' + r1.title = DmsfFileRevision.filename_to_title(r1.name) + r1.description = nil + r1.comment = nil + r1.mime_type = nil + r1.size = 4 + assert r1.invalid? + message = ['Attachment extension .png is not allowed'] + assert_equal message, r1.errors.full_messages + end + end + + def test_workflow_tooltip + @revision2.set_workflow @wf1.id, 'start' + assert_equal @jsmith.name, @revision2.workflow_tooltip + end + + def test_version + @revision1.major_version = 1 + @revision1.minor_version = 0 + assert_equal '1.0', @revision1.version + @revision1.major_version = -'A'.ord + @revision1.minor_version = -' '.ord + assert_equal 'A', @revision1.version + @revision1.major_version = -'A'.ord + @revision1.minor_version = 0 + assert_equal 'A.0', @revision1.version + end + + def test_increase_version + # 1.0.0 -> 1.0.1 + @revision1.major_version = 1 + @revision1.minor_version = 0 + @revision1.increase_version DmsfFileRevision::PATCH_VERSION + assert_equal 1, @revision1.major_version + assert_equal 0, @revision1.minor_version + assert_equal 1, @revision1.patch_version + # 1.0 -> 1.1 + @revision1.major_version = 1 + @revision1.minor_version = 0 + @revision1.increase_version DmsfFileRevision::MINOR_VERSION + assert_equal 1, @revision1.major_version + assert_equal 1, @revision1.minor_version + # 1.0 -> 2.0 + @revision1.major_version = 1 + @revision1.minor_version = 0 + @revision1.increase_version DmsfFileRevision::MAJOR_VERSION + assert_equal 2, @revision1.major_version + assert_equal 0, @revision1.minor_version + # 1.1 -> 2.0 + @revision1.major_version = 1 + @revision1.minor_version = 1 + @revision1.increase_version DmsfFileRevision::MAJOR_VERSION + assert_equal 2, @revision1.major_version + assert_equal 0, @revision1.minor_version + # A -> A.1 + @revision1.major_version = -'A'.ord + @revision1.minor_version = -' '.ord + @revision1.increase_version DmsfFileRevision::MINOR_VERSION + assert_equal(-'A'.ord, @revision1.major_version) + assert_equal 1, @revision1.minor_version + # A -> B + @revision1.major_version = -'A'.ord + @revision1.minor_version = -' '.ord + @revision1.increase_version DmsfFileRevision::MAJOR_VERSION + assert_equal(-'B'.ord, @revision1.major_version) + assert_equal(-' '.ord, @revision1.minor_version) + # A.1 -> B + @revision1.major_version = -'A'.ord + @revision1.minor_version = 1 + @revision1.increase_version DmsfFileRevision::MAJOR_VERSION + assert_equal(-'B'.ord, @revision1.major_version) + assert_equal(-' '.ord, @revision1.minor_version) + end + + def test_description_max_length + @revision1.description = 'a' * 2.kilobytes + assert_not @revision1.save + @revision1.description = 'a' * 1.kilobyte + assert @revision1.save + end + + def test_protocol_txt + assert_not @revision1.protocol + end + + def test_protocol_doc + @revision1.mime_type = Redmine::MimeType.of('test.doc') + assert_equal 'ms-word', @revision1.protocol + end + + def test_protocol_docx + @revision1.mime_type = Redmine::MimeType.of('test.docx') + assert_equal 'ms-word', @revision1.protocol + end + + def test_protocol_odt + @revision1.mime_type = Redmine::MimeType.of('test.odt') + assert_equal 'ms-word', @revision1.protocol + end + + def test_protocol_xls + @revision1.mime_type = Redmine::MimeType.of('test.xls') + assert_equal 'ms-excel', @revision1.protocol + end + + def test_protocol_xlsx + @revision1.mime_type = Redmine::MimeType.of('test.xlsx') + assert_equal 'ms-excel', @revision1.protocol + end + + def test_protocol_ods + @revision1.mime_type = Redmine::MimeType.of('test.ods') + assert_equal 'ms-excel', @revision1.protocol + end + + def test_obsolete + assert @revision1.obsolete + assert_equal DmsfWorkflow::STATE_OBSOLETE, @revision1.workflow + end + + def test_obsolete_locked + User.current = @admin + @revision1.dmsf_file.lock! + User.current = @jsmith + assert_not @revision1.obsolete + assert_equal 1, @revision1.errors.count + assert @revision1.errors.full_messages.to_sentence.include?(l(:error_file_is_locked)) + end + + def test_major_version_cannot_be_nil + @revision1.major_version = nil + assert_not @revision1.save + assert @revision1.errors.full_messages.to_sentence.include?('Major version cannot be blank') + end + + def test_size_validation + Setting.attachment_max_size = '1' + @revision1.size = 2.kilobytes + assert_not @revision1.valid? + end + + def test_visible + @revision1.deleted = DmsfFileRevision::STATUS_ACTIVE + assert @revision1.visible? + assert @revision1.visible?(@jsmith) + @revision1.deleted = DmsfFileRevision::STATUS_DELETED + assert_not @revision1.visible? + assert_not @revision1.visible?(@jsmith) + end + + def test_params_to_hash + parameters = ActionController::Parameters.new({ + '78': 'A', + '90': { + 'blank': '', + '1': { + 'filename': 'file.txt', + 'token': 'atoken' + } + } + }) + h = DmsfFileRevision.params_to_hash(parameters) + assert h.is_a?(Hash) + assert_equal 'atoken', h['90'][:token] + end + + def test_params_to_hash_empty_attachment + parameters = ActionController::Parameters.new({ + '78': 'A', + '90': { + 'blank': '', + '1': { + 'file': '' + } + } + }) + h = DmsfFileRevision.params_to_hash(parameters) + assert h.is_a?(Hash) + assert_nil h['90'] + end + + def test_set_workflow + @revision2.set_workflow @wf1.id, 'assign' + assert_equal DmsfWorkflow::STATE_ASSIGNED, @revision2.workflow + assert_equal User.current, @revision2.dmsf_workflow_assigned_by_user + assert @revision2.dmsf_workflow_assigned_at + @revision2.set_workflow @wf1.id, 'start' + assert_equal DmsfWorkflow::STATE_WAITING_FOR_APPROVAL, @revision2.workflow + assert_equal User.current, @revision2.dmsf_workflow_started_by_user + assert @revision2.dmsf_workflow_started_at + end + + def test_reset_workflow + @revision2.set_workflow @wf1.id, 'assign' + @revision2.set_workflow @wf1.id, 'start' + @revision2.reset_workflow + assert_nil @revision2.workflow + assert_nil @revision2.dmsf_workflow_id + assert_nil @revision2.dmsf_workflow_assigned_by_user_id + assert_nil @revision2.dmsf_workflow_assigned_at + assert_nil @revision2.dmsf_workflow_started_by_user_id + assert_nil @revision2.dmsf_workflow_started_at + end +end diff --git a/test/unit/dmsf_file_test.rb b/test/unit/dmsf_file_test.rb new file mode 100644 index 00000000..d293e1f7 --- /dev/null +++ b/test/unit/dmsf_file_test.rb @@ -0,0 +1,341 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# File tests +class DmsfFileTest < RedmineDmsf::Test::UnitTest + def setup + super + @issue1 = Issue.find 1 + @wf1 = DmsfWorkflow.find 1 + @wf2 = DmsfWorkflow.find 2 + end + + def test_file_name_length_validation + file = DmsfFile.new(name: Array.new(256).map { 'a' }.join) + assert file.invalid? + assert_equal ['Name is too long (maximum is 255 characters)'], file.errors.full_messages + end + + def test_project_file_count_differs_from_project_visibility_count + assert_not_same @project1.dmsf_files.all.size, @project1.dmsf_files.visible.all.size + end + + def test_project_dmsf_file_listing_contains_deleted_items + assert @project1.dmsf_files.index(&:deleted?), 'Expected at least one deleted item in ' + end + + def test_project_dmsf_file_visible_listing_contains_no_deleted_items + assert @project1.dmsf_files.visible.index(&:deleted?).nil?, 'There is a deleted file, this was unexpected' + end + + def test_known_locked_file_responds_as_being_locked + assert @file2.locked?, "#{@file2.name} is not locked" + end + + def test_file_with_locked_folder_is_reported_as_locked + assert @file4.locked?, "#{@file4.name} is not locked" + end + + def test_file_with_folder_up_heirarchy_locked_is_reported_as_locked + assert @file5.locked?, "#{@file5.name} is not locked" + end + + def test_file_locked_is_not_locked_for_user_who_locked + User.current = @admin + assert_not @file2.locked_for_user?, "#{@file2.name} is locked for #{User.current}" + end + + def test_file_locked_is_locked_for_user_who_didnt_lock + User.current = @jsmith + assert @file2.locked_for_user?, "#{@file1.name} is locked for #{User.current}" + end + + def test_file_with_no_locks_reported_unlocked + assert_not @file1.locked? + end + + def test_delete_restore + assert_equal 1, @file4.dmsf_file_revisions.visible.all.size + assert_equal 2, @file4.referenced_links.visible.all.size + end + + def test_delete + User.current = @admin + @file4.dmsf_folder.unlock! + assert @file4.delete(commit: false), @file4.errors.full_messages.to_sentence + assert @file4.deleted?, "File #{@file4.name} is not deleted" + assert_equal 0, @file4.dmsf_file_revisions.visible.all.size + # Links should not be deleted + assert_equal 2, @file4.referenced_links.visible.all.size + end + + def test_restore + User.current = @admin + @file4.dmsf_folder.unlock! + assert @file4.delete(commit: false), @file4.errors.full_messages.to_sentence + @file4.restore + assert_not @file4.deleted?, "File #{@file4} hasn't been restored" + assert_equal 1, @file4.dmsf_file_revisions.visible.all.size + assert_equal 2, @file4.referenced_links.visible.all.size + end + + def test_destroy + User.current = @admin + @file4.dmsf_folder.unlock! + assert_equal 1, @file4.dmsf_file_revisions.visible.all.size + assert_equal 2, @file4.referenced_links.visible.all.size + @file4.delete commit: true + assert_equal 0, @file4.dmsf_file_revisions.all.size + assert_equal 0, @file4.referenced_links.all.size + end + + def test_copy_to_filename + assert_no_difference '@file1.dmsf_file_revisions.count' do + new_file = @file1.copy_to_filename(@file1.project, nil, 'new_file.txt') + assert_not_equal new_file.id, @file1.id + assert_nil new_file.dmsf_folder_id + assert_nil @file1.dmsf_folder_id + assert_not_equal new_file.name, @file1.name + assert_equal new_file.dmsf_file_revisions.all.size, 1 + assert_nil new_file.last_revision.workflow + assert_nil new_file.last_revision.dmsf_workflow_id + assert_nil new_file.last_revision.dmsf_workflow_assigned_by_user_id + assert_nil new_file.last_revision.dmsf_workflow_assigned_at + assert_nil new_file.last_revision.dmsf_workflow_started_by_user_id + assert_nil new_file.last_revision.dmsf_workflow_started_at + end + end + + def test_copy_to_filename_with_global_workflow + @file1.last_revision.set_workflow(@wf2.id, nil) + @file1.last_revision.assign_workflow(@wf2.id) + new_file = @file1.copy_to_filename(@project2, nil, 'new_file.txt') + assert_equal DmsfWorkflow::STATE_ASSIGNED, new_file.last_revision.workflow + assert_equal @wf2.id, new_file.last_revision.dmsf_workflow_id + assert_equal User.current, new_file.last_revision.dmsf_workflow_assigned_by_user + assert new_file.last_revision.dmsf_workflow_assigned_at + assert_nil new_file.last_revision.dmsf_workflow_started_by_user_id + assert_nil new_file.last_revision.dmsf_workflow_started_at + end + + def test_copy_to_filename_with_workflow_to_the_same_project + @file7.last_revision.set_workflow(@wf1.id, nil) + @file7.last_revision.assign_workflow(@wf1.id) + new_file = @file7.copy_to_filename(@project1, nil, 'new_file.txt') + assert_equal DmsfWorkflow::STATE_ASSIGNED, new_file.last_revision.workflow + assert_equal @wf1.id, new_file.last_revision.dmsf_workflow_id + assert_equal User.current, new_file.last_revision.dmsf_workflow_assigned_by_user + assert new_file.last_revision.dmsf_workflow_assigned_at + assert_nil new_file.last_revision.dmsf_workflow_started_by_user_id + assert_nil new_file.last_revision.dmsf_workflow_started_at + end + + def test_copy_to_filename_with_workflow_to_other_project + @file7.last_revision.set_workflow(@wf1.id, nil) + @file7.last_revision.assign_workflow(@wf1.id) + new_file = @file7.copy_to_filename(@project2, nil, 'new_file.txt') + assert new_file + assert_nil new_file.last_revision.workflow + assert_nil new_file.last_revision.dmsf_workflow_id + assert_nil new_file.last_revision.dmsf_workflow_assigned_by_user_id + assert_nil new_file.last_revision.dmsf_workflow_assigned_at + assert_nil new_file.last_revision.dmsf_workflow_started_by_user_id + assert_nil new_file.last_revision.dmsf_workflow_started_at + end + + def test_copy_to + assert_no_difference '@file1.dmsf_file_revisions.count' do + new_file = @file1.copy_to(@file1.project, @folder1) + assert_not_equal new_file.id, @file1.id + assert_not_equal @file1.dmsf_folder_id, @folder1.id + assert_equal new_file.dmsf_folder_id, @folder1.id + assert_equal new_file.name, @file1.name + assert_equal new_file.dmsf_file_revisions.all.size, 1 + end + end + + def test_project_project + project = @file1.project + assert_not_nil project + assert project.is_a?(Project) + end + + def test_project_issue + project = @file7.project + assert_not_nil project + assert project.is_a?(Project) + end + + def test_disposition + # Text + assert_equal 'attachment', @file1.disposition + # Image + assert_equal 'inline', @file7.disposition + # PDF + assert_equal 'inline', @file8.disposition + # Video + @file1.last_revision.disk_filename = 'test.mp4' + assert_equal 'inline', @file1.disposition + # HTML + @file1.last_revision.disk_filename = 'test.html' + assert_equal 'inline', @file1.disposition + end + + def test_image + assert_not @file1.image? + assert @file7.image? + assert_not @file8.image? + end + + def test_text + assert @file1.text? + assert_not @file7.text? + assert_not @file8.text? + @file1.last_revision.disk_filename = 'test.c' + assert @file1.text? + end + + def test_pdf + assert_not @file1.pdf? + assert_not @file7.pdf? + assert @file8.pdf? + end + + def test_video + assert_not @file1.video? + @file1.last_revision.disk_filename = 'test.mp4' + assert @file1.video? + end + + def test_html + assert_not @file1.html? + @file1.last_revision.disk_filename = 'test.html' + assert @file1.html? + end + + def test_markdown + assert_not @file1.markdown? + @file1.last_revision.disk_filename = 'test.md' + assert @file1.markdown? + end + + def test_textile + assert_not @file1.textile? + @file1.last_revision.disk_filename = 'test.textile' + assert @file1.textile? + end + + def test_findn_file_by_name + assert DmsfFile.find_file_by_name(@project1, nil, 'test.txt') + assert_nil DmsfFile.find_file_by_name(@project1, nil, 'test.ods') + assert DmsfFile.find_file_by_name(@issue1, nil, 'test.pdf') + assert_nil DmsfFile.find_file_by_name(@issue1, nil, 'test.ods') + end + + def test_storage_path + with_settings plugin_redmine_dmsf: { 'dmsf_storage_directory' => 'files/dmsf' } do + sp = DmsfFile.storage_path + assert_kind_of Pathname, sp + assert_equal Rails.root.join(Setting.plugin_redmine_dmsf['dmsf_storage_directory']).to_s, sp.to_s + end + end + + def test_owner + assert @file1.owner?(@file1.last_revision.user) + assert_not @file1.owner?(@jsmith) + @file1.last_revision.user = @jsmith + assert @file1.owner?(@jsmith) + end + + def test_involved + assert @file1.involved?(@file1.last_revision.user) + assert_not @file1.involved?(@jsmith) + @file1.dmsf_file_revisions[1].user = @jsmith + assert @file1.involved?(@jsmith) + end + + def test_assigned + assert_not @file1.assigned?(@admin) + assert_not @file1.assigned?(@jsmith) + @file7.last_revision.set_workflow @wf1.id, nil + @file7.last_revision.assign_workflow @wf1.id + assert @file7.assigned?(@admin) + assert @file7.assigned?(@jsmith) + end + + def test_locked_by + # Locked file + assert_equal @admin.name, @file2.locked_by + # Unlocked file + assert_equal '', @file1.locked_by + end + + def test_watchable + @file1.add_watcher @jsmith + assert @file1.watched_by?(@jsmith) + end + + def test_office_doc + assert @file13.office_doc? + end + + def test_previewable + assert(@file13.previewable?) if RedmineDmsf::Preview.office_available? + end + + def test_pdf_preview + assert_not_empty(@file13.pdf_preview) if RedmineDmsf::Preview.office_available? + assert_empty @file1.pdf_preview + end + + def test_move_to_author + assert_equal @admin.id, @file1.last_revision.user_id + User.current = @jsmith + assert @file1.move_to(@folder1.project, @folder1) + assert_equal @admin.id, @file1.last_revision.user_id, "Author mustn't be updated when moving" + end + + def test_copy_to_author + assert_equal @admin.id, @file1.last_revision.user_id + User.current = @jsmith + f = @file1.copy_to(@folder1.project, @folder1) + assert f + assert_equal @jsmith.id, f.last_revision.user_id, 'Author must be updated when copying' + end + + def test_approval_allowed_zero_minor_yes + with_settings plugin_redmine_dmsf: { 'only_approval_zero_minor_version' => '1' } do + @file1.last_revision.minor_version = 0 + assert @file1.approval_allowed_zero_minor + @file1.last_revision.minor_version = 1 + assert_not @file1.approval_allowed_zero_minor + end + end + + def test_approval_allowed_zero_minor_no + with_settings plugin_redmine_dmsf: { 'only_approval_zero_minor_version' => '0' } do + @file1.last_revision.minor_version = 0 + assert @file1.approval_allowed_zero_minor + @file1.last_revision.minor_version = 1 + assert @file1.approval_allowed_zero_minor + end + end +end diff --git a/test/unit/dmsf_folder_permission_test.rb b/test/unit/dmsf_folder_permission_test.rb new file mode 100644 index 00000000..a56ba087 --- /dev/null +++ b/test/unit/dmsf_folder_permission_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Folder permissions tests +class DmsfFolderPermissionTest < RedmineDmsf::Test::UnitTest + def setup + super + @permission1 = DmsfFolderPermission.find 1 + end + + def test_scope + assert_equal 2, DmsfFolderPermission.all.size + end + + def test_scope_users + assert_equal 1, DmsfFolderPermission.users.all.size + end + + def test_scope_roles + assert_equal 1, DmsfFolderPermission.roles.all.size + end + + def test_copy_to + permission = @permission1.copy_to(@folder1) + assert permission + assert_equal @folder1.id, permission.dmsf_folder_id + assert_equal @permission1.object_id, permission.object_id + assert_equal @permission1.object_type, permission.object_type + end +end diff --git a/test/unit/dmsf_folder_test.rb b/test/unit/dmsf_folder_test.rb new file mode 100644 index 00000000..3fe5e2f5 --- /dev/null +++ b/test/unit/dmsf_folder_test.rb @@ -0,0 +1,322 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Folder tests +class DmsfFolderTest < RedmineDmsf::Test::UnitTest + def setup + super + @link2 = DmsfLink.find 2 + end + + def test_visibility + # The role has got permissions + User.current = @jsmith + assert_equal 7, DmsfFolder.where(project_id: @project1.id).all.size + assert_equal 5, DmsfFolder.visible.where(project_id: @project1.id).all.size + # The user has got permissions + User.current = @dlopper + # Hasn't got permissions for @folder7 + @folder7.dmsf_folder_permissions.where(object_type: 'User').delete_all + assert_equal 4, DmsfFolder.visible.where(project_id: @project1.id).all.size + # Anonymous user + User.current = User.anonymous + @project1.add_default_member User.anonymous + assert_equal 5, DmsfFolder.visible.where(project_id: @project1.id).all.size + end + + def test_visible + assert @folder1.visible? + assert @folder1.visible?(@jsmith) + @folder1.deleted = DmsfFolder::STATUS_DELETED + class << @folder1 + attr_accessor :type + end + @folder1.type = 'folder' + assert_not @folder1.visible? + end + + def test_permissions + User.current = @dlopper + assert DmsfFolder.permissions?(@folder7) + @folder7.dmsf_folder_permissions.where(object_type: 'User').delete_all + @folder7.reload + assert_not DmsfFolder.permissions?(@folder7) + end + + def test_permissions_to_system_folder + User.current = @jsmith + assert DmsfFolder.permissions?(@folder8) + end + + def test_delete + assert @folder6.delete(commit: false), @folder6.errors.full_messages.to_sentence + assert @folder6.deleted?, "Folder #{@folder6} hasn't been deleted" + end + + def test_delete_recursively + assert @folder1.delete(commit: false), @folder1.errors.full_messages.to_sentence + # First&second level + [@folder1, @folder2].each do |folder| + assert_equal folder.dmsf_folders.all.size, folder.dmsf_folders.collect(&:deleted?).size + assert_equal folder.dmsf_files.all.size, folder.dmsf_files.collect(&:deleted?).size + assert_equal folder.dmsf_links.all.size, folder.dmsf_links.collect(&:deleted?).size + end + end + + def test_restore + assert @folder6.delete(commit: false), @folder6.errors.full_messages.to_sentence + assert @folder6.deleted?, "Folder #{@folder6} hasn't been deleted" + assert @folder6.restore, @folder6.errors.full_messages.to_sentence + assert_not @folder6.deleted?, "Folder #{@folder6} hasn't been restored" + end + + def test_restore_recursively + # Delete + assert @folder1.delete(commit: false), @folder1.errors.full_messages.to_sentence + # Restore + assert @folder1.restore, @folder1.errors.full_messages.to_sentence + assert_not @folder1.deleted?, "Folder #{@folder1} hasn't been restored" + # First level + assert_not @folder2.deleted?, "Folder #{@folder2} hasn't been restored" + assert_not @link2.deleted?, "Link #{@link2} hasn't been restored" + # Second level + assert_not @file4.deleted?, "File #{@file4} hasn't been restored" + end + + def test_destroy + folder6_id = @folder6.id + @folder6.delete commit: true + assert_nil DmsfFolder.find_by(id: folder6_id) + end + + def test_destroy_recursively + folder1_id = @folder1.id + folder2_id = @folder2.id + link2_id = @link2.id + file4_id = @file4.id + @folder1.delete commit: true + assert_nil DmsfFolder.find_by(id: folder1_id) + # First level + assert_nil DmsfFolder.find_by(id: folder2_id) + assert_nil DmsfLink.find_by(id: link2_id) + # Second level + assert_nil DmsfFile.find_by(id: file4_id) + end + + def test_is_column_on_default + DmsfFolder::DEFAULT_COLUMNS.each do |column| + assert DmsfFolder.column_on?(column), "The column #{column} is not on?" + end + end + + def test_is_column_on_available + (DmsfFolder::AVAILABLE_COLUMNS - DmsfFolder::DEFAULT_COLUMNS).each do |column| + assert_not DmsfFolder.column_on?(column), "The column #{column} is on?" + end + end + + def test_get_column_position_default + # 0 - checkbox + assert_nil DmsfFolder.get_column_position('checkbox'), "The column 'checkbox' is on?" + # 1 - id + assert_nil DmsfFolder.get_column_position('id'), "The column 'id' is on?" + # 2 - title + assert_equal DmsfFolder.get_column_position('title'), 1, "The expected position of the 'title' column is 1" + # 3 - size + assert_equal DmsfFolder.get_column_position('size'), 2, "The expected position of the 'size' column is 2" + # 4 - modified + assert_equal DmsfFolder.get_column_position('modified'), 3, "The expected position of the 'modified' column is 3" + # 5 - version + assert_equal DmsfFolder.get_column_position('version'), 4, "The expected position of the 'version' column is 4" + # 6 - workflow + assert_equal DmsfFolder.get_column_position('workflow'), 5, "The expected position of the 'workflow' column is 5" + # 7 - author + assert_equal DmsfFolder.get_column_position('author'), 6, "The expected position of the 'workflow' column is 6" + # 8 - custom fields + assert_nil DmsfFolder.get_column_position('Tag'), "The column 'Tag' is on?" + # 9 - commands + assert_equal DmsfFolder.get_column_position('commands'), 7, "The expected position of the 'commands' column is 7" + # 10 - position + assert_equal DmsfFolder.get_column_position('position'), 8, "The expected position of the 'position' column is 8" + # 11 - size + assert_equal DmsfFolder.get_column_position('size_calculated'), 9, + "The expected position of the 'size_calculated' column is 9" + # 12 - modified + assert_equal DmsfFolder.get_column_position('modified_calculated'), 10, + "The expected position of the 'modified_calculated' column is 10" + # 13 - version + assert_equal DmsfFolder.get_column_position('version_calculated'), 11, + "The expected position of the 'version_calculated' column is 11" + end + + def test_directory_tree + User.current = @admin + @folder7.lock! + User.current = @jsmith + assert @folder7.locked_for_user? + tree = DmsfFolder.directory_tree(@project1) + assert tree + # [["Documents", nil], + # ["...folder1", 1], + # ["......folder2", 2] + # [".........folder5", 5], + # ["...folder6", 6]] + # ["...folder7", 7] - locked + assert tree.to_s.include?('...folder1'), "'...folder1' string in the folder tree expected." + assert_not tree.to_s.include?('...folder7'), "'...folder7' string in the folder tree not expected." + end + + def test_directory_tree_id + User.current = @admin + @folder7.lock! + User.current = @jsmith + assert @folder7.locked_for_user? + tree = DmsfFolder.directory_tree(@project1.id) + assert tree + # [["Documents", nil], + # ["...folder1", 1], + # ["......folder2", 2] + # [".........folder5", 5], + # ["...folder6", 6]] + # ["...folder7", 7] - locked + assert tree.to_s.include?('...folder1'), "'...folder1' string in the folder tree expected." + assert_not tree.to_s.include?('...folder7'), "'...folder7' string in the folder tree not expected." + end + + def test_folder_tree + User.current = @admin + tree = @folder1.folder_tree + assert tree + # [["folder1", 1], + # ["...folder2", 2] - locked for admin + assert tree.to_s.include?('folder1'), "'folder1' string in the folder tree expected." + assert_not tree.to_s.include?('...folder2'), "'...folder2' string in the folder tree not expected." + end + + def test_get_valid_title + assert_equal '1052-6024 . U_CPLD_5M240Z_SMT_MBGA100_1.8V_-40', + DmsfFolder.get_valid_title('1052-6024 : U_CPLD_5M240Z_SMT_MBGA100_1.8V_-40...') + assert_equal 'test', DmsfFolder.get_valid_title("test#{DmsfFolder::INVALID_CHARACTERS}") + end + + def test_permission_for_role + checked = @folder7.permission_for_role(@manager_role) + assert checked + end + + def test_permissions_users + users = @folder7.permissions_users + assert_equal 1, users.size + end + + def test_move_to + User.current = @jsmith + assert @folder1.move_to(@project2, nil) + assert_equal @project2, @folder1.project + @folder1.dmsf_folders.each do |d| + assert_equal @project2.identifier, d.project.identifier + end + @folder1.dmsf_files.each do |f| + assert_equal @project2, f.project + end + @folder1.dmsf_links.each do |l| + assert_equal @project2, l.project + end + end + + def test_copy_to + assert @folder1.copy_to(@project2, nil) + assert DmsfFolder.find_by(project_id: @project2.id, title: @folder1.title) + end + + def test_move_to_author + assert_equal @admin.id, @folder1.user_id + User.current = @jsmith + assert @folder1.move_to(@folder6.project, @folder6) + assert_equal @admin.id, @folder1.user_id, "Author mustn't be updated when moving" + end + + def test_copy_to_author + assert_equal @admin.id, @folder1.user_id + User.current = @jsmith + f = @folder1.copy_to(@folder6.project, @folder6) + assert f + assert_equal @jsmith.id, f.user_id, 'Author must be updated when copying' + end + + def test_valid_parent + @folder2.dmsf_folder = @folder1 + assert @folder2.save + end + + def test_valid_parent_nil + @folder2.dmsf_folder = nil + assert @folder2.save + end + + def test_valid_parent_loop + @folder1.dmsf_folder = @folder2 + # @folder2 is under @folder1 => loop! + assert_not @folder1.save + end + + def test_empty + assert_not @folder1.empty? + assert @folder6.empty? + end + + def test_watchable + @folder1.add_watcher @jsmith + assert @folder1.watched_by?(@jsmith) + end + + def test_update_from_params_with_invalid_string_sequence + invalid_string_sequence = "Invalid sequence\x81" + params = { dmsf_folder: { title: invalid_string_sequence, description: invalid_string_sequence } } + assert @folder1.update_from_params(params) + assert_equal invalid_string_sequence.scrub, @folder1.title + assert_equal invalid_string_sequence.scrub, @folder1.description + end + + def test_issystem + assert DmsfFolder.where(id: @folder8.id).issystem.exists? + @folder8.deleted = DmsfFolder::STATUS_DELETED + assert @folder8.save + assert_not DmsfFolder.where(id: @folder8.id).issystem.exists? + end + + def test_any_child + # folder1 + ## folder 2 + # folder6 + assert_not @folder1.any_child?(@folder6) + assert @folder1.any_child?(@folder2) + end + + def delete_system_folder + # System folders + assert DmsfFolder.where(id: [8, 9]).exist? + @file_link7.destroy + @dmsf_file11.destroy + # System folders are empty and should have been deleted + assert_not DmsfFolder.where(id: [8, 9]).exist? + end +end diff --git a/test/unit/dmsf_link_test.rb b/test/unit/dmsf_link_test.rb new file mode 100644 index 00000000..50ad085e --- /dev/null +++ b/test/unit/dmsf_link_test.rb @@ -0,0 +1,227 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Link tests +class DmsfLinksTest < RedmineDmsf::Test::UnitTest + def test_create_folder_link + folder_link = DmsfLink.new + folder_link.target_project_id = @project1.id + folder_link.target_id = @folder1.id + folder_link.target_type = DmsfFolder.model_name.to_s + folder_link.name = 'folder1_link2' + folder_link.project_id = @project1.id + folder_link.created_at = DateTime.current + folder_link.updated_at = DateTime.current + assert folder_link.save, folder_link.errors.full_messages.to_sentence + end + + def test_create_file_link + file_link = DmsfLink.new + file_link.target_project_id = @project1.id + file_link.target_id = @file1.id + file_link.target_type = DmsfFile.model_name.to_s + file_link.name = 'file1_link2' + file_link.project_id = @project1.id + file_link.created_at = DateTime.current + file_link.updated_at = DateTime.current + assert file_link.save, file_link.errors.full_messages.to_sentence + end + + def test_create_external_link + external_link = DmsfLink.new + external_link.target_project_id = @project1.id + external_link.external_url = 'http://www.redmine.org/plugins/dmsf' + external_link.target_type = 'DmsfUrl' + external_link.name = 'DMSF plugin' + external_link.project_id = @project1.id + external_link.created_at = DateTime.current + external_link.updated_at = DateTime.current + assert external_link.save, external_link.errors.full_messages.to_sentence + end + + def test_validate_name_length + @folder_link1.name = ('a' * 256) + assert_not @folder_link1.save, "Folder link #{@folder_link1.name} should have not been saved" + assert_equal 1, @folder_link1.errors.size + end + + def test_validate_name_presence + @folder_link1.name = '' + assert_not @folder_link1.save, "Folder link #{@folder_link1.name} should have not been saved" + assert_equal 1, @folder_link1.errors.size + end + + def test_validate_external_url_length + @file_link2.target_type = 'DmsfUrl' + @file_link2.external_url = "https://localhost/#{'a' * 256}" + assert_not @file_link2.save, "External URL link #{@file_link2.name} should have not been saved" + assert_equal 1, @file_link2.errors.size + end + + def test_validate_external_url_presence + @file_link2.target_type = 'DmsfUrl' + @file_link2.external_url = '' + assert_not @file_link2.save, "External URL link #{@file_link2.name} should have not been saved" + assert_equal 1, @file_link2.errors.size + end + + def test_validate_external_url_invalid + @file_link2.target_type = 'DmsfUrl' + @file_link2.external_url = 'htt ps://abc.xyz' + assert_not @file_link2.save, "External URL link #{@file_link2.name} should have not been saved" + assert_equal 1, @file_link2.errors.size + end + + def test_validate_external_url_valid + @file_link2.target_type = 'DmsfUrl' + @file_link2.external_url = 'https://www.google.com/search?q=寿司' + assert @file_link2.save + end + + def test_belongs_to_project + @project1.destroy + assert_nil DmsfLink.find_by(id: 1) + assert_nil DmsfLink.find_by(id: 2) + end + + def test_belongs_to_dmsf_folder + @folder1.destroy + assert_nil DmsfLink.find_by(id: 1) + assert_nil DmsfLink.find_by(id: 2) + end + + def test_target_folder_id + assert_equal 2, @file_link2.target_folder_id + assert_equal 1, @folder_link1.target_folder_id + end + + def test_target_folder + assert_equal @folder2, @file_link2.target_folder + assert_equal @folder1, @folder_link1.target_folder + end + + def test_target_file_id + assert_equal 4, @file_link2.target_file_id + assert_nil @folder_link1.target_file + end + + def test_target_file + assert_equal @file4, @file_link2.target_file + assert_nil @folder_link1.target_file + end + + def test_target_project + assert_equal @project1, @file_link2.target_project + assert_equal @project1, @folder_link1.target_project + end + + def test_folder + assert_equal @folder1, @file_link2.dmsf_folder + assert_nil @folder_link1.dmsf_folder + end + + def test_title + assert_equal @file_link2.name, @file_link2.title + assert_equal @folder_link1.name, @folder_link1.title + end + + def test_find_link_by_file_name + file_link = DmsfLink.find_link_by_file_name(@file_link2.project, @file_link2.dmsf_folder, + @file_link2.target_file.name) + assert file_link, 'File link not found by its name' + end + + def test_path + assert_equal 'folder1/folder2/test4.txt', @file_link2.path + assert_equal 'folder1', @folder_link1.path + end + + def test_file_kink_copy_to + file_link_copy = @file_link2.copy_to @folder2.project, @folder2 + assert_not_nil file_link_copy, 'File link copying failed' + assert_equal file_link_copy.target_project_id, @file_link2.target_project_id + assert_equal file_link_copy.target_id, @file_link2.target_id + assert_equal file_link_copy.target_type, @file_link2.target_type + assert_equal file_link_copy.name, @file_link2.name + assert_equal file_link_copy.project_id, @folder2.project.id + assert_equal file_link_copy.dmsf_folder_id, @folder2.id + end + + def test_folder_link_copy_to + folder_link_copy = @folder_link1.copy_to @folder2.project, @folder2 + assert_not_nil folder_link_copy, 'Folder link copying failed' + assert_equal folder_link_copy.target_project_id, @folder_link1.target_project_id + assert_equal folder_link_copy.target_id, @folder_link1.target_id + assert_equal folder_link_copy.target_type, @folder_link1.target_type + assert_equal folder_link_copy.name, @folder_link1.name + assert_equal folder_link_copy.project_id, @folder2.project.id + assert_equal folder_link_copy.dmsf_folder_id, @folder2.id + end + + def test_move_to_author + assert_equal @admin.id, @file_link2.user_id + User.current = @jsmith + assert @file_link2.move_to(@project1, @folder1) + assert_equal @admin.id, @file_link2.user_id, "Author mustn't be updated when moving" + end + + def test_copy_to_author + assert_equal @admin.id, @file_link2.user_id + User.current = @jsmith + l = @file_link2.copy_to(@project1, @folder1) + assert l + assert_equal @jsmith.id, l.user_id, 'Author must be updated when copying' + end + + def test_delete_file_link + assert @file_link2.delete(commit: false), @file_link2.errors.full_messages.to_sentence + assert @file_link2.deleted?, "File link hasn't been deleted" + end + + def test_restore_file_link + assert @file_link2.delete(commit: false), @file_link2.errors.full_messages.to_sentence + assert @file_link2.deleted?, "File link hasn't been deleted" + assert @file_link2.restore, @file_link2.errors.full_messages.to_sentence + assert_not @file_link2.deleted?, "File link hasn't been restored" + end + + def test_delete_folder_link + assert @folder_link1.delete(commit: false), @folder_link1.errors.full_messages.to_sentence + assert @folder_link1.deleted?, "Folder link hasn't been deleted" + end + + def test_restore_folder_link + assert @folder_link1.delete(commit: false), @folder_link1.errors.full_messages.to_sentence + assert @folder_link1.deleted?, "Folder link hasn't been deleted" + assert @folder_link1.restore, @folder_link1.errors.full_messages.to_sentence + assert_not @folder_link1.deleted?, "Folder link hasn't been restored" + end + + def test_destroy_file_link + assert @file_link2.delete(commit: true), @file_link2.errors.full_messages.to_sentence + assert_nil DmsfLink.find_by(id: @file_link2.id) + end + + def test_destroy_folder_link + assert @folder_link1.delete(commit: true), @folder_link1.errors.full_messages.to_sentence + assert_nil DmsfLink.find_by(id: @folder_link1.id) + end +end diff --git a/test/unit/dmsf_lock_test.rb b/test/unit/dmsf_lock_test.rb new file mode 100644 index 00000000..edceac8b --- /dev/null +++ b/test/unit/dmsf_lock_test.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper.rb', __FILE__) + +# Lock tests +class DmsfLockTest < RedmineDmsf::Test::UnitTest + def setup + super + @lock = DmsfLock.find 1 + end + + def test_lock_type_is_enumerable + assert DmsfLock.respond_to?(:lock_types), "DmsfLock class hasn't got lock_types method" + assert DmsfLock.lock_types.is_a?(SimpleEnum::Enum), 'DmsfLock class is not enumerable' + end + + def test_lock_scope_is_enumerable + assert DmsfLock.respond_to?(:lock_scopes), "DmsfLock class hasn't got lock_scopes method" + assert DmsfLock.lock_scopes.is_a?(SimpleEnum::Enum), 'DmsfLock class is not enumerable' + end + + def test_linked_to_either_file_or_folder + assert_not_nil @lock.dmsf_file || @lock.dmsf_folder + if @lock.dmsf_file + assert_kind_of DmsfFile, @lock.dmsf_file + else + assert_kind_of DmsfFolder, @lock.dmsf_folder + end + end + + def test_locked_folder_reports_un_locked_child_file_as_locked + assert @folder2.locked?, "Folder #{@folder2.title} should be locked by fixture" + assert_equal 1, @folder2.lock.count # Check the folder lists 1 lock + assert @file4.locked?, "File #{@file4.name} sits within #{@folder2.title} and should be locked" + assert_equal 1, @file4.lock.count # Check the file lists 1 lock + assert_equal 0, @file4.lock(tree: false).count # Check the file does not list any entries for itself + end + + def test_locked_folder_cannot_be_unlocked_by_someone_without_rights_or_anon + User.current = @admin + @folder7.lock! + User.current = nil + assert_no_difference('@folder7.lock.count') do + assert_raise DmsfLockError do + @folder7.unlock! + end + end + User.current = @jsmith + assert_no_difference('@folder7.lock.count') do + assert_raise DmsfLockError do + @folder7.unlock! + end + end + end + + def test_locked_folder_can_be_unlocked_by_permission + User.current = @admin + assert_difference('@folder2.lock.count', -1) do + assert_nothing_raised do + @folder2.unlock! + end + end + User.current = @jsmith + assert_difference('@folder2.lock.count') do + assert_nothing_raised do + @folder2.lock! + end + end + User.current = @admin + assert_difference('@folder2.lock.count', -1) do + assert_nothing_raised do + @folder2.unlock! + end + end + @folder2.lock! + end + + def test_expired + User.current = @jsmith + lock = DmsfLock.new + assert_not lock.expired? + lock.expires_at = Time.current + assert lock.expired? + lock.expires_at = 1.hour.from_now + assert_not lock.expired? + end +end diff --git a/test/unit/dmsf_mailer_test.rb b/test/unit/dmsf_mailer_test.rb new file mode 100644 index 00000000..50ade2bc --- /dev/null +++ b/test/unit/dmsf_mailer_test.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Mailer tests +class DmsfMailerTest < RedmineDmsf::Test::UnitTest + include Redmine::I18n + + def setup + super + @file1.notify_activate + @wf1 = DmsfWorkflow.find 1 + @rev2 = DmsfFileRevision.find 2 + # Mailer settings + ActionMailer::Base.deliveries.clear + Setting.plain_text_mail = '0' + Setting.default_language = 'en' + # Notification + m1 = Member.find 1 + m1.dmsf_mail_notification = true + m1.save + end + + def test_files_updated + DmsfMailer.deliver_files_updated(@file1.project, [@file1]) + email = last_email + return unless email # Sometimes it doesn't work. Especially on localhost. + + body = text_part(email)&.body + assert(body.include?(@file1.project.name)) if body + body = html_part(email)&.body + assert(body.include?(@file1.project.name)) if body + end + + def test_files_deleted + DmsfMailer.deliver_files_deleted(@file1.project, [@file1]) + email = last_email + return unless email # Sometimes it doesn't work. Especially on localhost. + + body = text_part(email)&.body + assert(body.include?(@file1.project.name)) if body + body = html_part(email)&.body + assert(body.include?(@file1.project.name)) if body + end + + def test_files_downloaded + DmsfMailer.deliver_files_downloaded(@file1.project, [@file1], '127.0.0.1') + email = last_email + return unless email # Sometimes it doesn't work. Especially on localhost. + + body = text_part(email)&.body + assert(body.include?(@file1.project.name)) if body + body = html_part(email)&.body + assert(body.include?(@file1.project.name)) if body + end + + def test_send_documents + email_params = {} + body = 'Test' + email_params[:to] = @jsmith.mail + email_params[:from] = @jsmith.mail + email_params[:body] = body + email_params[:links_only] = '1' + email_params[:public_urls] = '0' + email_params[:expired_at] = DateTime.current.to_s + email_params[:folders] = nil + email_params[:files] = "[\"#{@file1.id}\"]" + DmsfMailer.deliver_send_documents(@file1.project, email_params, @jsmith) + email = last_email + return unless email # Sometimes it doesn't work. Especially on localhost. + + body = text_part(email)&.body + assert(body.include?(body)) if body + body = html_part(email)&.body + assert(body.include?(body)) if body + end + + def test_workflow_notification + DmsfMailer.deliver_workflow_notification([@jsmith], @wf1, @rev2, :text_email_subject_started, + :text_email_started, :text_email_to_proceed) + email = last_email + return unless email # Sometimes it doesn't work. Especially on localhost. + + body = text_part(email)&.body + assert(body.include?(l(:text_email_subject_started))) if body + body = html_part(email)&.body + assert(body.include?(l(:text_email_subject_started))) if body + end + + def test_get_notify_users + with_settings notified_events: ['dmsf_legacy_notifications'] do + users = DmsfMailer.get_notify_users(@project1, @file1) + assert users.present? + end + with_settings notified_events: [] do + users = DmsfMailer.get_notify_users(@project1, @file1) + assert users.empty? + end + end + + def test_get_notify_users_notification_switched_off + @file1.notify_deactivate + users = DmsfMailer.get_notify_users(@project1, @file1) + assert users.blank? + end + + def test_get_notify_users_on_inactive_projects + @project1.status = Project::STATUS_CLOSED + users = DmsfMailer.get_notify_users(@project1, @file1) + assert users.blank? + end + + def test_get_notify_users_with_watchers + @file1.add_watcher @jsmith + with_settings notified_events: [] do + users = DmsfMailer.get_notify_users(@project1, @file1) + assert users.present? + end + end +end diff --git a/test/unit/dmsf_public_url_test.rb b/test/unit/dmsf_public_url_test.rb new file mode 100644 index 00000000..1e7d3e40 --- /dev/null +++ b/test/unit/dmsf_public_url_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Public URL tests +class DmsfPublicUrlsTest < RedmineDmsf::Test::UnitTest + def setup + super + @dmsf_public_url1 = DmsfPublicUrl.find 1 + end + + def test_create + url = DmsfPublicUrl.new + url.dmsf_file = @file1 + url.user = @jsmith + url.expire_at = DateTime.current + 1.day + assert url.save, url.errors.full_messages.to_sentence + assert_not_nil url.token + end + + def test_belongs_to_file + @file1.destroy + assert_nil DmsfPublicUrl.find_by(id: 1) + end +end diff --git a/test/unit/dmsf_query_test.rb b/test/unit/dmsf_query_test.rb new file mode 100644 index 00000000..f7c081c2 --- /dev/null +++ b/test/unit/dmsf_query_test.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Daniel Munn , Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Query tests +class DmsfQueryTest < RedmineDmsf::Test::UnitTest + def setup + super + @query401 = Query.find 401 + User.current = @jsmith + end + + def test_typetest_dmsf_nodes + assert_equal 'DmsfQuery', @query401.type + end + + def test_available_columns + n = DmsfQuery.available_columns.size + DmsfFileRevisionCustomField.visible.all.size + assert_equal n, @query401.available_columns.size + end + + def test_dmsf_count + n = DmsfFolder.visible.where(project_id: @project1.id).where("title LIKE '%test%'").all.size + + DmsfFile.visible.where(project_id: @project1.id).where("name LIKE '%test%'").all.size + + DmsfLink.visible.where(project_id: @project1.id).where("name LIKE '%test%'").all.size + assert_equal n - 1, @query401.dmsf_count # One folder is not visible due to the permissions + end + + def test_dmsf_nodes + assert @query401.dmsf_nodes.any? + end + + def test_default + # User + @jsmith.pref.default_dmsf_query = @query401.id + assert_equal @query401, DmsfQuery.default + @jsmith.pref.default_dmsf_query = nil + + # Project + @project1.default_dmsf_query_id = @query401.id + assert_equal @query401, DmsfQuery.default(@project1) + @project1.default_dmsf_query_id = nil + + # Global + with_settings plugin_redmine_dmsf: { 'dmsf_default_query' => @query401.id } do + assert_equal @query401, DmsfQuery.default + end + end + + def test_groupable_columns + assert_not @query401.groupable_columns.any? + end +end diff --git a/test/unit/dmsf_upload_test.rb b/test/unit/dmsf_upload_test.rb new file mode 100644 index 00000000..e7e56e9c --- /dev/null +++ b/test/unit/dmsf_upload_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Upload tests +class DmsfUploadTest < RedmineDmsf::Test::UnitTest + def setup + super + @uploaded = { + disk_filename: '230105094726_1-TIOMAN_IMG_4391a.JPG', + content_type: 'image/jpeg', + original_filename: '1-TIOMAN_IMG_4391a.JPG', + comment: '', + tempfile_path: '/opt/redmine/files/2023/01/230105104724_1-TIOMAN_IMG_4391a.JPG', + digest: '3fae7e571666c3b9a70067970a00396b5019e6ea94b26398d13e989e695a1a39' + } + end + + def test_initialize + with_settings plugin_redmine_dmsf: { 'empty_minor_version_by_default' => '1' } do + upload = DmsfUpload.new(@project1, nil, @uploaded) + assert_equal 1, upload.major_version + assert_nil upload.minor_version + end + with_settings plugin_redmine_dmsf: { 'empty_minor_version_by_default' => nil } do + upload = DmsfUpload.new(@project1, nil, @uploaded) + assert_equal 0, upload.major_version + assert_equal 0, upload.minor_version + end + end +end diff --git a/test/unit/dmsf_workflow_step_action_test.rb b/test/unit/dmsf_workflow_step_action_test.rb new file mode 100644 index 00000000..0931c240 --- /dev/null +++ b/test/unit/dmsf_workflow_step_action_test.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Workflow step actions tests +class DmsfWorkflowStepActionTest < RedmineDmsf::Test::UnitTest + include Redmine::I18n + + def setup + @wfsac1 = DmsfWorkflowStepAction.find 1 + @wfsac2 = DmsfWorkflowStepAction.find 2 + @wfsac3 = DmsfWorkflowStepAction.find 3 + end + + def test_create + wfsac = DmsfWorkflowStepAction.new + wfsac.dmsf_workflow_step_assignment_id = 1 + wfsac.action = DmsfWorkflowStepAction::ACTION_DELEGATE + wfsac.note = 'Approval' + assert wfsac.save, wfsac.errors.full_messages.to_sentence + wfsac.reload + assert wfsac.created_at + end + + def test_update + @wfsac1.dmsf_workflow_step_assignment_id = 2 + @wfsac1.action = DmsfWorkflowStepAction::ACTION_REJECT + @wfsac1.note = 'Rejection' + assert @wfsac1.save, !@wfsac1.errors.full_messages.to_sentence + @wfsac1.reload + assert_equal 2, @wfsac1.dmsf_workflow_step_assignment_id + assert_equal DmsfWorkflowStepAction::ACTION_REJECT, @wfsac1.action + assert_equal 'Rejection', @wfsac1.note + end + + def test_validate_action_presence + @wfsac1.action = nil + assert_not @wfsac1.save + assert_equal 1, @wfsac1.errors.count + end + + def test_validate_note + @wfsac1.note = '' + @wfsac1.action = DmsfWorkflowStepAction::ACTION_REJECT + assert_not @wfsac1.save + assert_equal 1, @wfsac1.errors.count + @wfsac1.note = 'Rejected because....' + assert @wfsac1.save + @wfsac1.action = DmsfWorkflowStepAction::ACTION_DELEGATE + @wfsac1.note = '' + assert_not @wfsac1.save + assert_equal 1, @wfsac1.errors.count + @wfsac1.note = 'Delegated because' + assert @wfsac1.save, @wfsac1.errors.full_messages.to_sentence + @wfsac1.note = '' + @wfsac1.action = DmsfWorkflowStepAction::ACTION_APPROVE + assert @wfsac1.save, @wfsac1.errors.full_messages.to_sentence + end + + def test_validate_dmsf_workflow_step_assignment_id_uniqueness + @wfsac2.dmsf_workflow_step_assignment_id = @wfsac1.dmsf_workflow_step_assignment_id + @wfsac2.action = @wfsac1.action + assert_not @wfsac2.save + assert_equal 1, @wfsac2.errors.count + @wfsac1.action = DmsfWorkflowStepAction::ACTION_REJECT + @wfsac2.action = @wfsac1.action + assert @wfsac1.save, @wfsac1.errors.full_messages.to_sentence + assert_not @wfsac2.save + assert_equal 1, @wfsac2.errors.count + @wfsac1.action = DmsfWorkflowStepAction::ACTION_DELEGATE + assert @wfsac1.save + @wfsac2.action = @wfsac1.action + assert @wfsac2.save + end + + def test_destroy + @wfsac1.destroy + assert_nil DmsfWorkflowStepAction.find_by(id: 1) + end + + def test_is_finished + @wfsac1.action = DmsfWorkflowStepAction::ACTION_APPROVE + assert @wfsac1.finished? + @wfsac1.action = DmsfWorkflowStepAction::ACTION_REJECT + assert @wfsac1.finished? + @wfsac1.action = DmsfWorkflowStepAction::ACTION_DELEGATE + assert_not @wfsac1.finished? + end + + def test_action_str + assert_equal DmsfWorkflowStepAction.action_str(DmsfWorkflowStepAction::ACTION_APPROVE), l(:title_approval) + assert_equal DmsfWorkflowStepAction.action_str(DmsfWorkflowStepAction::ACTION_REJECT), l(:title_rejection) + assert_equal DmsfWorkflowStepAction.action_str(DmsfWorkflowStepAction::ACTION_DELEGATE), l(:title_delegation) + assert_equal DmsfWorkflowStepAction.action_str(DmsfWorkflowStepAction::ACTION_ASSIGN), l(:title_assignment) + assert_equal DmsfWorkflowStepAction.action_str(DmsfWorkflowStepAction::ACTION_START), l(:title_start) + end +end diff --git a/test/unit/dmsf_workflow_step_assignment_test.rb b/test/unit/dmsf_workflow_step_assignment_test.rb new file mode 100644 index 00000000..b1104715 --- /dev/null +++ b/test/unit/dmsf_workflow_step_assignment_test.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Workflow step assignment tests +class WorkflowStepAssignmentTest < RedmineDmsf::Test::UnitTest + def setup + @wfsa1 = DmsfWorkflowStepAssignment.find 1 + @wfsa2 = DmsfWorkflowStepAssignment.find 2 + end + + def test_create + wfsa = DmsfWorkflowStepAssignment.new + wfsa.dmsf_workflow_step_id = 5 + wfsa.user_id = 2 + wfsa.dmsf_file_revision_id = 2 + assert wfsa.save, wfsa.errors.full_messages.to_sentence + end + + def test_update + @wfsa1.dmsf_workflow_step_id = 5 + @wfsa1.user_id = 2 + @wfsa1.dmsf_file_revision_id = 2 + assert @wfsa1.save, @wfsa1.errors.full_messages.to_sentence + @wfsa1.reload + assert_equal 5, @wfsa1.dmsf_workflow_step_id + assert_equal 2, @wfsa1.user_id + assert_equal 2, @wfsa1.dmsf_file_revision_id + end + + def test_validate_dmsf_workflow_step_id_uniqueness + @wfsa1.dmsf_workflow_step_id = @wfsa2.dmsf_workflow_step_id + @wfsa1.dmsf_file_revision_id = @wfsa2.dmsf_file_revision_id + assert_not @wfsa1.save + assert_equal 1, @wfsa1.errors.count + end + + def test_destroy + @wfsa1.destroy + assert_nil DmsfWorkflowStepAssignment.find_by(id: 1) + assert_nil DmsfWorkflowStepAction.find_by(id: 1) + end +end diff --git a/test/unit/dmsf_workflow_step_test.rb b/test/unit/dmsf_workflow_step_test.rb new file mode 100644 index 00000000..647d1755 --- /dev/null +++ b/test/unit/dmsf_workflow_step_test.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Workflow step +class DmsfWorkflowStepTest < RedmineDmsf::Test::UnitTest + include Redmine::I18n + + def setup + @wfs1 = DmsfWorkflowStep.find 1 + @wfs2 = DmsfWorkflowStep.find 2 + @wfs5 = DmsfWorkflowStep.find 5 + @revision1 = DmsfFileRevision.find 1 + @revision2 = DmsfFileRevision.find 2 + @wf2 = DmsfWorkflow.find 2 + end + + def test_create + wfs = DmsfWorkflowStep.new + wfs.dmsf_workflow_id = 1 + wfs.step = 2 + wfs.name = '2nd step' + wfs.user_id = 3 + wfs.operator = 1 + assert wfs.save, wfs.errors.full_messages.to_sentence + end + + def test_update + @wfs1.dmsf_workflow_id = 2 + @wfs1.step = 2 + @wfs1.name = '2nd step' + @wfs1.user_id = 2 + @wfs1.operator = 2 + assert @wfs1.save, @wfs1.errors.full_messages.to_sentence + @wfs1.reload + assert_equal 2, @wfs1.dmsf_workflow_id + assert_equal 2, @wfs1.step + assert_equal '2nd step', @wfs1.name + assert_equal 2, @wfs1.user_id + assert_equal 2, @wfs1.operator + end + + def test_validate_step_presence + @wfs1.step = nil + assert_not @wfs1.save + assert @wfs1.errors.any? + end + + def test_validate_operator_presence + @wfs1.operator = nil + assert_not @wfs1.save + assert @wfs1.errors.any? + end + + def test_validate_user_id_uniqueness + @wfs2.user_id = @wfs1.user_id + @wfs2.dmsf_workflow_id = @wfs1.dmsf_workflow_id + @wfs2.step = @wfs1.step + assert_not @wfs2.save + assert @wfs2.errors.any? + end + + def test_validate_name_length + @wfs1.name = 'a' * 31 + assert_not @wfs1.save + assert_equal 1, @wfs1.errors.count + end + + def test_destroy + assert_not DmsfWorkflowStepAssignment.where(dmsf_workflow_step_id: @wfs2.id).empty? + @wfs2.destroy + assert_nil DmsfWorkflowStep.find_by(id: 2) + assert_equal DmsfWorkflowStepAssignment.where(dmsf_workflow_step_id: @wfs2.id).all.size, 0 + end + + def test_soperator + assert_equal @wfs1.soperator, l(:dmsf_or) + end + + def test_user + assert_equal @wfs1.user, User.find_by(id: @wfs1.user_id) + end + + def test_assign + @wfs5.assign(@revision2.id) + assert DmsfWorkflowStepAssignment.exists?(dmsf_workflow_step_id: @wfs5.id, dmsf_file_revision_id: @revision2.id) + end + + def test_copy_to + wfs = @wfs1.copy_to(@wf2) + assert_equal wfs.dmsf_workflow_id, @wf2.id + assert_equal wfs.step, @wfs1.step + assert_equal wfs.name, @wfs1.name + assert_equal wfs.user_id, @wfs1.user_id + assert_equal wfs.operator, @wfs1.operator + end +end diff --git a/test/unit/dmsf_workflow_test.rb b/test/unit/dmsf_workflow_test.rb new file mode 100644 index 00000000..9b00257b --- /dev/null +++ b/test/unit/dmsf_workflow_test.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Workflow tests +class DmsfWorkflowTest < RedmineDmsf::Test::UnitTest + def setup + super + @wf1 = DmsfWorkflow.find 1 + @wf2 = DmsfWorkflow.find 2 + @wf3 = DmsfWorkflow.find 3 + @wfs1 = DmsfWorkflowStep.find 1 + @wfs2 = DmsfWorkflowStep.find 2 + @wfs3 = DmsfWorkflowStep.find 3 + @wfs4 = DmsfWorkflowStep.find 4 + @wfs5 = DmsfWorkflowStep.find 5 + @wfsa1 = DmsfWorkflowStepAssignment.find 1 + @wfsac1 = DmsfWorkflowStepAction.find 1 + @revision1 = DmsfFileRevision.find 1 + @revision2 = DmsfFileRevision.find 2 + end + + def test_create + workflow = DmsfWorkflow.new + workflow.name = 'wf' + assert workflow.save, workflow.errors.full_messages.to_sentence + end + + def test_update + @wf1.name = 'wf1a' + @wf1.project_id = @project5.id + assert @wf1.save, @wf1.errors.full_messages.to_sentence + @wf1.reload + assert_equal 'wf1a', @wf1.name + assert_equal @project5.id, @wf1.project_id + end + + def test_validate_name_length + @wf1.name = 'a' * 256 + assert_not @wf1.save + assert_equal 1, @wf1.errors.count + end + + def test_validate_name_presence + @wf1.name = '' + assert_not @wf1.save + assert_equal 1, @wf1.errors.count + end + + def test_validate_name_uniqueness_globaly + @wf2.name = @wf1.name + assert_not @wf2.save + assert_equal 1, @wf2.errors.count + end + + def test_validate_name_uniqueness_localy + @wf2.name = @wf1.name + @wf2.project_id = @wf1.project_id + assert_not @wf2.save + assert_equal 1, @wf2.errors.count + end + + def test_destroy + @wf1.destroy + assert_nil DmsfWorkflow.find_by(id: 1) + assert_nil DmsfWorkflowStep.find_by(id: @wfs1.id) + end + + def test_project + # Global workflow + assert_nil @wf2.project + # Project workflow + @wf2.project_id = @project5.id + assert @wf2.project + end + + def test_to_s + assert_equal @wf1.name, @wf1.to_s + end + + def test_reorder_steps_highest + @wf1.reorder_steps(3, 1) + assert_equal [2, 2, 3, 3, 1], @wf1.dmsf_workflow_steps.collect(&:step) + end + + def test_reorder_steps_higher + @wf1.reorder_steps(3, 2) + assert_equal [1, 1, 3, 3, 2], @wf1.dmsf_workflow_steps.collect(&:step) + end + + def test_reorder_steps_lower + @wf1.reorder_steps(1, 2) + assert_equal [2, 2, 1, 1, 3], @wf1.dmsf_workflow_steps.collect(&:step) + end + + def test_reorder_steps_lowest + @wf1.reorder_steps(1, 3) + assert_equal [3, 3, 1, 1, 2], @wf1.dmsf_workflow_steps.collect(&:step) + end + + def test_delegates + delegates = @wf1.delegates(nil, nil, nil) + assert_equal(delegates.size + 1, @project5.users.size) + delegates = @wf1.delegates('Dave', nil, nil) + assert_equal delegates.size, 1 + delegates = @wf1.delegates(nil, @wfsa1.id, 2) + assert_not(delegates.any? { |user| user.id == @wfsa1.user_id }) + assert(delegates.any? { |user| user.id == 8 }) + end + + def test_next_assignments + assignments = @wf1.next_assignments(2) + assert_equal assignments.size, 1 + assert_equal assignments[0].user_id, 2 + end + + def test_assign + @wf2.assign @revision2.id + @wf2.dmsf_workflow_steps.each do |step| + assert DmsfWorkflowStepAssignment.exists?(dmsf_workflow_step_id: step.id, dmsf_file_revision_id: @revision2.id) + end + end + + def test_try_finish_yes + # The forkflow is waiting for an approval + assert_equal DmsfWorkflow::STATE_WAITING_FOR_APPROVAL, @revision1.workflow + # Do the approval + wsa = DmsfWorkflowStepAction.new + wsa.dmsf_workflow_step_assignment_id = 9 + wsa.action = DmsfWorkflowStepAction::ACTION_APPROVE + wsa.author_id = @jsmith.id + assert wsa.save + # The workflow is finished + assert @wf1.try_finish?(@revision1, @wfsac1, @jsmith.id) + @revision1.reload + assert_equal DmsfWorkflow::STATE_APPROVED, @revision1.workflow + end + + def test_try_finish_no + # The forkflow is waiting for an approval + assert_equal DmsfWorkflow::STATE_WAITING_FOR_APPROVAL, @revision1.workflow + # The workflow is not finished + assert_not @wf1.try_finish?(@revision1, @wfsac1, @jsmith.id) + @revision1.reload + assert_equal DmsfWorkflow::STATE_WAITING_FOR_APPROVAL, @revision1.workflow + end + + def test_participiants + assert_equal @wf1.participiants.count, 2 + end + + def test_locked + assert @wf3.locked?, "#{@wf2.name} status is #{@wf3.status}" + end + + def test_active + assert @wf1.active?, "#{@wf1.name} status is #{@wf1.status}" + end + + def test_scope_active + assert_equal DmsfWorkflow.count, (DmsfWorkflow.active.all.size + 1) + end + + def test_scope_status + assert_equal 1, DmsfWorkflow.status(DmsfWorkflow::STATUS_LOCKED).all.size + end + + def test_copy_to + wf = @wf1.copy_to(@project5, "#{@wf1.name}_copy") + assert_equal wf.project_id, @project5.id + assert_equal wf.name, "#{@wf1.name}_copy" + end +end diff --git a/test/unit/issue_patch_test.rb b/test/unit/issue_patch_test.rb new file mode 100644 index 00000000..5149f017 --- /dev/null +++ b/test/unit/issue_patch_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# Issue tests +class IssuePatchTest < RedmineDmsf::Test::UnitTest + def setup + @issue1 = Issue.find 1 + end + + def test_issue_has_dmsf_files + assert @issue1.respond_to?(:dmsf_files) + end +end diff --git a/test/unit/lib/acccess_control_patch_test.rb b/test/unit/lib/acccess_control_patch_test.rb new file mode 100644 index 00000000..09727845 --- /dev/null +++ b/test/unit/lib/acccess_control_patch_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# AccessControl patch tests +class AccessControlPatchTest < RedmineDmsf::Test::UnitTest + def test_available_project_modules + assert Redmine::AccessControl.available_project_modules.include?(:documents) + with_settings plugin_redmine_dmsf: { 'remove_original_documents_module' => '1' } do + assert_not Redmine::AccessControl.available_project_modules.include?(:documents) + end + end +end diff --git a/test/unit/lib/redmine_dmsf/dmsf_macros_test.rb b/test/unit/lib/redmine_dmsf/dmsf_macros_test.rb new file mode 100644 index 00000000..e76969ea --- /dev/null +++ b/test/unit/lib/redmine_dmsf/dmsf_macros_test.rb @@ -0,0 +1,455 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../../test_helper', __FILE__) + +# Macros tests +class DmsfMacrosTest < RedmineDmsf::Test::HelperTest + # Mock view context for macros + class DmsfView + include ApplicationHelper + include ActionView::Helpers + include ActionDispatch::Routing + include ERB::Util + include Rails.application.routes.url_helpers + end + + # Cache the view context to avoid creating it for each macro call + def dmsf_view_context + @dmsf_view_context ||= DmsfView.new + end + + # Hack to bypass missing methods to mocked view context + def respond_to_missing?(name, include_private) + dmsf_view_context.respond_to?(name) || super + end + + def method_missing(method_name, ...) + dmsf_view_context.send(method_name.to_s, ...) + end + + def setup + super + User.current = @jsmith + Rails.application.routes.default_url_options[:host] = 'www.example.com' + @file1 = DmsfFile.find_by(id: 1) + @file6 = DmsfFile.find_by(id: 6) # video + @file7 = DmsfFile.find_by(id: 7) # image + @folder1 = DmsfFolder.find_by(id: 1) + end + + # {{dmsf(file_id [, title [, revision_id]])}} + def test_macro_dmsf + text = textilizable("{{dmsf(#{@file1.id})}}") + assert text.include?(@file1.title), text + end + + def test_macro_dmsf_file_not_found + text = textilizable('{{dmsf(99)}}') + assert text.include?('{{dmsf(99)}}'), text + end + + def test_macro_dmsf_no_permissions + @manager_role.remove_permission! :view_dmsf_files + text = textilizable("{{dmsf(#{@file1.id})}}") + assert text.exclude?(@file1.title), text + end + + def test_macro_dmsf_dmsf_off + @project1.disable_module! :dmsf + text = textilizable("{{dmsf(#{@file1.id})}}") + assert text.exclude?(@file1.title), text + end + + def test_macro_dmsf_custom_title + text = textilizable("{{dmsf(#{@file1.id}, xyz)}}") + assert text.include?('xyz'), text + end + + def test_macro_dmsf_custom_title_aprostrophes + text = textilizable("{{dmsf(#{@file1.id}, 'xyz')}}") + assert text.include?('xyz'), text + end + + def test_macro_dmsf_custom_title_and_revision + text = textilizable("{{dmsf(#{@file1.id}, '', 1)}}") + assert text.include?('download=1'), text + end + + # {{dmsff([folder_id [, title]])}} + def test_macro_dmsff + text = textilizable("{{dmsff(#{@folder1.id})}}") + assert text.include?(@folder1.title), text + end + + def test_macro_dmsff_no_permissions + @manager_role.remove_permission! :view_dmsf_folders + text = textilizable("{{dmsf(#{@folder1.id})}}") + assert text.exclude?(@folder1.title), text + end + + def test_macro_dmsff_dmsf_off + @project1.disable_module! :dmsf + text = textilizable("{{dmsf(#{@folder1.id})}}") + assert text.exclude?(@folder1.title), text + end + + def test_macro_dmsff_custom_title + text = textilizable("{{dmsf(#{@folder1.id}, xyz)}}") + assert text.include?('xyz'), text + end + + def test_macro_dmsff_custom_title_aprostrophes + text = textilizable("{{dmsf(#{@folder1.id}, 'xyz')}}") + assert text.include?('xyz'), text + end + + # {{dmsfd(document_id [, title])}} + def test_macro_dmsfd + text = textilizable("{{dmsfd(#{@file1.id})}}") + assert text.include?(@file1.title), text + end + + def test_macro_dmsfd_no_permissions + @manager_role.remove_permission! :view_dmsf_files + text = textilizable("{{dmsfd(#{@file1.id})}}") + assert text.exclude?(@file1.title), text + end + + def test_macro_dmsfd_dmsf_off + @project1.disable_module! :dmsf + text = textilizable("{{dmsfd(#{@file1.id})}}") + assert text.exclude?(@file1.title), text + end + + def test_macro_dmsfd_custom_title + text = textilizable("{{dmsfd(#{@file1.id}, xyz)}}") + assert text.include?('xyz'), text + end + + def test_macro_dmsfd_custom_title_aprostrophes + text = textilizable("{{dmsfd(#{@file1.id}, 'xyz')}}") + assert text.include?('xyz'), text + end + + # {{dmsfdesc(document_id)}} + def test_macro_dmsfdesc + rev = @file1.last_revision + rev.description = 'blabla' + rev.save + text = textilizable("{{dmsfdesc(#{@file1.id})}}") + assert text.include?(rev.description), text + end + + def test_macro_dmsfdesc_no_permissions + @manager_role.remove_permission! :view_dmsf_files + rev = @file1.last_revision + rev.description = 'blabla' + rev.save + text = textilizable("{{dmsfdesc(#{@file1.id})}}") + assert text.exclude?(rev.description), text + end + + def test_macro_dmsfdesc_dmsf_off + @project1.disable_module! :dmsf + rev = @file1.last_revision + rev.description = 'blabla' + rev.save + text = textilizable("{{dmsfdesc(#{@file1.id})}}") + assert text.exclude?(rev.description), text + end + + # {{dmsfversion(document_id [, revision_id])}} + def test_macro_dmsfdversion + text = textilizable("{{dmsfversion(#{@file1.id})}}") + assert text.include?(@file1.version), text + end + + def test_macro_dmsfdversion_revision + revision5 = DmsfFileRevision.find_by(id: 5) + text = textilizable("{{dmsfversion(#{@file1.id}, #{revision5.id})}}") + assert text.include?(revision5.version), text + end + + def test_macro_dmsfdversion_no_permissions + @manager_role.remove_permission! :view_dmsf_files + text = textilizable("{{dmsfversion(#{@file1.id})}}") + assert text.exclude?(@file1.version), text + end + + def test_macro_dmsfdversion_dmsf_off + @project1.disable_module! :dmsf + text = textilizable("{{dmsfversion(#{@file1.id})}}") + assert text.exclude?(@file1.version), text + end + + # {{dmsflastupdate(document_id)}} + def test_macro_dmsflastupdate + text = textilizable("{{dmsflastupdate(#{@file1.id})}}") + assert text.include?(format_time(@file1.last_revision.updated_at)), text + end + + def test_macro_dmsflastupdate_no_permissions + @manager_role.remove_permission! :view_dmsf_files + text = textilizable("{{dmsflastupdate(#{@file1.id})}}") + assert text.exclude?(format_time(@file1.last_revision.updated_at)), text + end + + def test_macro_dmsflastupdate_dmsf_off + @project1.disable_module! :dmsf + text = textilizable("{{dmsflastupdate(#{@file1.id})}}") + assert text.exclude?(format_time(@file1.last_revision.updated_at)), text + end + + # {{dmsft(document_id)}} + def test_macro_dmsft + text = textilizable("{{dmsft(#{@file1.id}, 1)}}") + assert text.include?(content_tag(:pre, @file1.text_preview(1))), text + end + + def test_macro_dmsft_no_permissions + @manager_role.remove_permission! :view_dmsf_files + text = textilizable("{{dmsft(#{@file1.id}, 1)}}") + assert text.exclude?(content_tag(:pre, @file1.text_preview(1))), text + end + + def test_macro_dmsft_dmsf_off + @project1.disable_module! :dmsf + text = textilizable("{{dmsft(#{@file1.id}, 1)}}") + assert text.exclude?(content_tag(:pre, @file1.text_preview(1))), text + end + + # {{dmsf_image(file_id)}} + def test_macro_dmsf_image + url = static_dmsf_file_url(@file7, @file7.last_revision.name) + text = textilizable("{{dmsf_image(#{@file7.id})}}") + assert text.include?(image_tag(url, alt: @file7.name, title: @file7.title, size: nil)), text + end + + # {{dmsf_image(file_id file_id)}} + def test_macro_dmsf_image_multiple + url = static_dmsf_file_url(@file7, @file7.last_revision.name) + text = textilizable("{{dmsf_image(#{@file7.id} #{@file7.id})}}") + link = image_tag(url, alt: @file7.name, title: @file7.title, size: nil) + assert text.include?(link + link), text + end + + def test_macro_dmsf_image_size + size = '50%' + url = static_dmsf_file_url(@file7, @file7.last_revision.name) + text = textilizable("{{dmsf_image(#{@file7.id}, size=#{size})}}") + assert text.include?(image_tag(url, alt: @file7.name, title: @file7.title, width: size, height: size)), text + # TODO: arguments src and with and height are swapped + # size = '300' + # text = textilizable("{{dmsf_image(#{@file7.id}, size=#{size})}}") + # assert text.include?(image_tag(url, alt: @file7.name, title: @file7.title, width: size, height: size)), text + # TODO: arguments src and with and height are swapped + # size = '640x480' + # text = textilizable("{{dmsf_image(#{@file7.id}, size=#{size})}}") + # assert text.include?(image_tag(url, alt: @file7.name, title: @file7.title, width: '640', height: '480')), text + height = '480' + text = textilizable("{{dmsf_image(#{@file7.id}, height=#{height})}}") + assert text.include?(image_tag(url, alt: @file7.name, title: @file7.title, width: 'auto', height: height)), text + width = '480' + text = textilizable("{{dmsf_image(#{@file7.id}, width=#{height})}}") + assert text.include?(image_tag(url, alt: @file7.name, title: @file7.title, width: width, height: 'auto')), text + end + + def test_macro_dmsf_image_no_permissions + @manager_role.remove_permission! :view_dmsf_files + url = static_dmsf_file_url(@file7, @file7.last_revision.name) + text = textilizable("{{dmsf_image(#{@file7.id})}}") + assert text.exclude?(image_tag(url, alt: @file7.name, title: @file7.title, size: nil)), text + end + + def test_macro_dmsf_image_dmsf_off + @project1.disable_module! :dmsf + url = static_dmsf_file_url(@file7, @file7.last_revision.name) + text = textilizable("{{dmsf_image(#{@file7.id})}}") + assert text.exclude?(image_tag(url, alt: @file7.name, title: @file7.title, size: nil)), text + end + + def test_macro_dmsf_image_not_image + text = textilizable("{{dmsf_image(#{@file1.id})}}") + assert text.include?(::I18n.t(:error_not_supported_image_format)) + end + + # {{dmsf_video(file_id)}} + def test_macro_dmsf_video + text = textilizable("{{dmsf_video(#{@file6.id})}}") + url = static_dmsf_file_url(@file6, @file6.last_revision.name) + assert text.include?(video_tag(url, controls: true, alt: @file6.name, title: @file6.title)), text + end + + def test_macro_dmsf_video_size + size = '50%' + url = static_dmsf_file_url(@file6, @file6.last_revision.name) + text = textilizable("{{dmsf_video(#{@file6.id}, size=#{size})}}") + link = video_tag(url, controls: true, alt: @file6.name, title: @file6.title, width: size, height: size) + assert text.include?(link), text + size = '300' + text = textilizable("{{dmsf_video(#{@file6.id}, size=#{size})}}") + link = video_tag(url, controls: true, alt: @file6.name, title: @file6.title, width: size, height: size) + assert text.include?(link), text + size = '640x480' + text = textilizable("{{dmsf_video(#{@file6.id}, size=#{size})}}") + link = video_tag(url, controls: true, alt: @file6.name, title: @file6.title, width: '640', height: '480') + assert text.include?(link), text + height = '480' + text = textilizable("{{dmsf_video(#{@file6.id}, height=#{height})}}") + link = video_tag(url, controls: true, alt: @file6.name, title: @file6.title, width: 'auto', height: height) + assert text.include?(link), text + width = '480' + text = textilizable("{{dmsf_video(#{@file6.id}, width=#{height})}}") + link = video_tag(url, controls: true, alt: @file6.name, title: @file6.title, width: width, height: 'auto') + assert text.include?(link), text + end + + def test_macro_dmsf_video_no_permissions + @developer_role.remove_permission! :view_dmsf_files + text = textilizable("{{dmsf_video(#{@file6.id})}}") + url = static_dmsf_file_url(@file6, @file6.last_revision.name) + assert text.exclude?(video_tag(url, controls: true, alt: @file6.name, title: @file6.title)), text + end + + def test_macro_dmsf_video_dmsf_off + @project2.disable_module! :dmsf + text = textilizable("{{dmsf_video(#{@file6.id})}}") + url = static_dmsf_file_url(@file6, @file6.last_revision.name) + assert text.exclude?(video_tag(url, controls: true, alt: @file6.name, title: @file6.title)), text + end + + def test_macro_dmsf_video_not_video + text = textilizable("{{dmsf_video(#{@file7.id})}}") + assert text.include?(::I18n.t(:error_not_supported_video_format)), text + end + + # {{dmsftn(file_id)}} + def test_macro_dmsftn + text = textilizable("{{dmsftn(#{@file7.id})}}") + url = static_dmsf_file_url(@file7, @file7.last_revision.name) + img = image_tag(url, alt: @file7.name, title: @file7.title, width: 'auto', height: 200) + link = link_to(img, + url, + target: '_blank', + rel: 'noopener', + title: h(@file7.last_revision.try(:tooltip)), + 'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}") + assert text.include?(link), text + end + + # {{dmsftn(file_id file_id)}} + def test_macro_dmsftn_multiple + text = textilizable("{{dmsftn(#{@file7.id} #{@file7.id})}}") + url = static_dmsf_file_url(@file7, @file7.last_revision.name) + img = image_tag(url, alt: @file7.name, title: @file7.title, width: 'auto', height: 200) + link = link_to(img, + url, + target: '_blank', + rel: 'noopener', + title: h(@file7.last_revision.try(:tooltip)), + 'data-downloadurl': 'image/gif:test.gif:http://www.example.com/dmsf/files/7/test.gif') + assert text.include?(link + link), text + end + + # {{dmsftn(file_id size=300)}} + def test_macro_dmsftn_size + url = static_dmsf_file_url(@file7, @file7.last_revision.name) + size = '300' + text = textilizable("{{dmsftn(#{@file7.id}, size=#{size})}}") + img = image_tag(url, alt: @file7.name, title: @file7.title, size: size) + link = link_to(img, + url, + target: '_blank', + rel: 'noopener', + title: h(@file7.last_revision.try(:tooltip)), + 'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}") + assert text.include?(link), text + # TODO: arguments src and with and height are swapped + # size = '640x480' + # text = textilizable("{{dmsftn(#{@file7.id}, size=#{size})}}") + # img = image_tag(url, alt: @file7.name, title: @file7.title, width: 640, height: 480) + # link = link_to(img, + # url, + # target: '_blank', + # rel: 'noopener', + # title: h(@file7.last_revision.try(:tooltip)), + # 'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}") + # assert text.include?(link), text + height = '480' + text = textilizable("{{dmsftn(#{@file7.id}, height=#{height})}}") + img = image_tag(url, alt: @file7.name, title: @file7.title, width: 'auto', height: 480) + link = link_to(img, + url, + target: '_blank', + rel: 'noopener', + title: h(@file7.last_revision.try(:tooltip)), + 'data-downloadurl': 'image/gif:test.gif:http://www.example.com/dmsf/files/7/test.gif') + assert text.include?(link), text + width = '640' + text = textilizable("{{dmsftn(#{@file7.id}, width=#{width})}}") + img = image_tag(url, alt: @file7.name, title: @file7.title, width: 640, height: 'auto') + link = link_to(img, + url, + target: '_blank', + rel: 'noopener', + title: h(@file7.last_revision.try(:tooltip)), + 'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}") + assert text.include?(link), text + end + + def test_macro_dmsftn_no_permissions + @manager_role.remove_permission! :view_dmsf_files + text = textilizable("{{dmsftn(#{@file7.id})}}") + url = view_dmsf_file_url(@file7) + img = image_tag(url, alt: @file7.name, title: @file7.title, width: 'auto', height: 200) + assert text.exclude?(link_to(img, url, title: h(@file7.last_revision.try(:tooltip)))), text + end + + def test_macro_dmsftn_dmsf_off + @project1.disable_module! :dmsf + text = textilizable("{{dmsftn(#{@file7.id})}}") + url = view_dmsf_file_url(@file7) + img = image_tag(url, alt: @file7.name, title: @file7.title, width: 'auto', height: 200) + assert text.exclude?(link_to(img, url, title: h(@file7.last_revision.try(:tooltip)))), text + end + + def test_macro_dmsftn_not_image + text = textilizable("{{dmsftn(#{@file1.id})}}") + assert text.include?(::I18n.t(:error_not_supported_image_format)) + end + + # {{dmsfw(file_id)}} + def test_macro_dmsfw + text = textilizable("{{dmsfw(#{@file1.id})}}") + assert text.include?(@file1.last_revision.workflow_str(false)), text + end + + def test_macro_dmsfw_no_permissions + @manager_role.remove_permission! :view_dmsf_files + text = textilizable("{{dmsfw(#{@file1.id})}}") + assert text.include?(::I18n.t(:notice_not_authorized)) + end + + def test_macro_dmsfw_dmsf_off + @project1.disable_module! :dmsf + text = textilizable("{{dmsfw(#{@file1.id})}}") + assert text.include?(::I18n.t(:notice_not_authorized)) + end +end diff --git a/test/unit/lib/redmine_dmsf/dmsf_plugin_test.rb b/test/unit/lib/redmine_dmsf/dmsf_plugin_test.rb new file mode 100644 index 00000000..55ce667f --- /dev/null +++ b/test/unit/lib/redmine_dmsf/dmsf_plugin_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../../test_helper', __FILE__) +require File.expand_path('../../../../../lib/redmine_dmsf/plugin', __FILE__) + +# Plugin tests +class DmsfPluginTest < RedmineDmsf::Test::HelperTest + def test_present_yes + assert RedmineDmsf::Plugin.present?(:redmine_dmsf) + end + + def test_present_no + assert_not RedmineDmsf::Plugin.present?(:redmine_dmsfx) + end + + def test_an_obsolete_plugin_present_no + # No such plugin is present + assert_not RedmineDmsf::Plugin.an_obsolete_plugin_present? + end + + def test_an_obsolete_plugin_present_yes + # Create a fake redmine_checklists plugin + path = Rails.root.join('plugins/redmine_resources') + FileUtils.mkdir_p path + assert RedmineDmsf::Plugin.an_obsolete_plugin_present? + FileUtils.rm_rf path + end + + def test_lib_available? + assert RedmineDmsf::Plugin.lib_available?('zip') + assert_not RedmineDmsf::Plugin.lib_available?('not_existing_gem') + end +end diff --git a/test/unit/lib/redmine_dmsf/dmsf_zip_test.rb b/test/unit/lib/redmine_dmsf/dmsf_zip_test.rb new file mode 100644 index 00000000..8f72de4d --- /dev/null +++ b/test/unit/lib/redmine_dmsf/dmsf_zip_test.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../../test_helper', __FILE__) +require File.expand_path('../../../../../lib/redmine_dmsf/dmsf_zip', __FILE__) + +# Plugin tests +class DmsfZipTest < RedmineDmsf::Test::HelperTest + def setup + @zip = RedmineDmsf::DmsfZip::Zip.new + + Setting.plugin_redmine_dmsf['dmsf_storage_directory'] = File.join('files', 'dmsf') + FileUtils.cp_r File.join(File.expand_path('../../../../fixtures/files', __FILE__), '.'), DmsfFile.storage_path + @dmsf_file1 = DmsfFile.find(1) + @dmsf_folder2 = DmsfFolder.find(2) + set_fixtures_attachments_directory + @attachment6 = Attachment.find(6) + end + + def teardown + # Delete our tmp folder + FileUtils.rm_rf DmsfFile.storage_path + rescue StandardError => e + Rails.logger.error e.message + end + + def test_add_dmsf_file + @zip.add_dmsf_file @dmsf_file1 + assert_equal 1, @zip.dmsf_files.size + zip_file = @zip.finish + Zip::File.open(zip_file) do |file| + file.each do |entry| + assert_equal @dmsf_file1.last_revision.name, entry.name + end + end + end + + def test_add_attachment + assert File.exist?(@attachment6.diskfile), @attachment6.diskfile + @zip.add_attachment @attachment6, @attachment6.filename + assert_equal 0, @zip.dmsf_files.size + zip_file = @zip.finish + Zip::File.open(zip_file) do |file| + file.each do |entry| + assert_equal @attachment6.filename, entry.name + end + end + end + + def test_add_raw_file + filename = 'data.txt' + content = '1,2,3' + @zip.add_raw_file filename, content + assert_equal 0, @zip.dmsf_files.size + zip_file = @zip.finish + Zip::File.open(zip_file) do |file| + file.each do |entry| + assert_equal filename, entry.name + assert_equal content, entry.get_input_stream.read + end + end + end + + def test_read + @zip.add_dmsf_file @dmsf_file1 + assert_not_empty @zip.read + end + + def test_finish + @zip.add_dmsf_file @dmsf_file1 + zip_file = @zip.finish + assert File.exist?(zip_file) + end + + def test_close + @zip.add_dmsf_file @dmsf_file1 + assert @zip.close + end +end diff --git a/test/unit/lib/search_patch_test.rb b/test/unit/lib/search_patch_test.rb new file mode 100644 index 00000000..2ae83036 --- /dev/null +++ b/test/unit/lib/search_patch_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../../test_helper', __FILE__) + +# Search patch tests +class SearchPatchTest < RedmineDmsf::Test::UnitTest + def test_available_search_types + assert Redmine::Search.available_search_types.include?('documents') + with_settings plugin_redmine_dmsf: { 'remove_original_documents_module' => '1' } do + assert_not Redmine::Search.available_search_types.include?('documents') + end + end +end diff --git a/test/unit/project_patch_test.rb b/test/unit/project_patch_test.rb new file mode 100644 index 00000000..49060d9d --- /dev/null +++ b/test/unit/project_patch_test.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# project tests +class ProjectPatchTest < RedmineDmsf::Test::UnitTest + def test_project_has_dmsf_files + assert @project1.respond_to?(:dmsf_files) + end + + def test_project_has_dmsf_folders + assert @project1.respond_to?(:dmsf_folders) + end + + def test_project_has_dmsf_workflows + assert @project1.respond_to?(:dmsf_workflows) + end + + def test_project_has_folder_links + assert @project1.respond_to?(:folder_links) + end + + def test_project_has_file_links + assert @project1.respond_to?(:file_links) + end + + def test_project_has_url_links + assert @project1.respond_to?(:url_links) + end + + def test_project_has_dmsf_links + assert @project1.respond_to?(:dmsf_links) + end + + def test_project_has_default_dmsf_query + assert @project1.respond_to?(:default_dmsf_query) + end + + def test_dmsf_count + User.current = @jsmith + hash = @project1.dmsf_count + assert_equal 9, hash[:files] + assert_equal 5, hash[:folders] + end + + def test_copy_approval_workflows + assert_equal 1, @project1.dmsf_workflows.all.size + assert_equal 0, @project2.dmsf_workflows.all.size + @project2.copy_approval_workflows @project1 + assert_equal 1, @project2.dmsf_workflows.all.size + end + + def test_copy_dmsf + User.current = @jsmith + + assert_equal 5, @project1.dmsf_files.visible.all.size + assert_equal 3, @project1.dmsf_folders.visible.all.size + assert_equal 2, @project1.file_links.visible.all.size + assert_equal 1, @project1.folder_links.visible.all.size + assert_equal 0, @project1.url_links.visible.all.size + + assert_equal 1, @project5.dmsf_files.visible.all.size + assert_equal 1, @project5.dmsf_folders.visible.all.size + assert_equal 0, @project5.file_links.visible.all.size + assert_equal 0, @project5.folder_links.visible.all.size + assert_equal 0, @project5.url_links.visible.all.size + + @project5.copy_dmsf @project1 + + assert_equal 6, @project5.dmsf_files.visible.all.size + assert_equal 4, @project5.dmsf_folders.visible.all.size + assert_equal 2, @project5.file_links.visible.all.size + assert_equal 1, @project5.folder_links.visible.all.size + assert_equal 0, @project5.url_links.visible.all.size + end + + def test_dmsf_avaliable + # @project1 (:dmsf, manager) + # L @project3 (:dmsf), @project4, @project5 + User.current = @jsmith + assert @project1.dmsf_available? + assert_not @project3.dmsf_available? + end + + def test_watchable + @project1.add_watcher @jsmith + assert @project1.watched_by?(@jsmith) + end +end diff --git a/test/unit/user_patch_test.rb b/test/unit/user_patch_test.rb new file mode 100644 index 00000000..310092be --- /dev/null +++ b/test/unit/user_patch_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# User tests +class UserPatchTest < RedmineDmsf::Test::UnitTest + def test_remove_dmsf_references + id = @jsmith.id + @jsmith.destroy + assert_equal 0, DmsfFileRevisionAccess.where(user_id: id).all.size + assert_equal 0, DmsfFileRevision.where(user_id: id).all.size + assert_equal 0, DmsfFileRevision.where(dmsf_workflow_assigned_by_user_id: id).all.size + assert_equal 0, DmsfFileRevision.where(dmsf_workflow_started_by_user_id: id).all.size + assert_equal 0, DmsfFile.where(deleted_by_user_id: id).all.size + assert_equal 0, DmsfFolder.where(deleted_by_user_id: id).all.size + assert_equal 0, DmsfLink.where(user_id: id).all.size + assert_equal 0, DmsfLink.where(deleted_by_user_id: id).all.size + assert_equal 0, DmsfFolder.where(user_id: id).all.size + assert_equal 0, DmsfLock.where(user_id: id).all.size + assert_equal 0, DmsfWorkflowStepAction.where(author_id: id).all.size + assert_equal 0, DmsfWorkflowStepAssignment.where(user_id: id).all.size + assert_equal 0, DmsfWorkflowStep.where(user_id: id).all.size + assert_equal 0, DmsfWorkflow.where(author_id: id).all.size + assert_equal 0, DmsfFolderPermission.where(object_id: id, object_type: 'User').all.size + end +end diff --git a/test/unit/user_preference_patch_test.rb b/test/unit/user_preference_patch_test.rb new file mode 100644 index 00000000..bbfdad45 --- /dev/null +++ b/test/unit/user_preference_patch_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Karel Pičman +# +# 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 +# . + +require File.expand_path('../../test_helper', __FILE__) + +# User preference tests +class UserPreferencePatchTest < RedmineDmsf::Test::UnitTest + def test_user_preference_has_dmsf_attachments_upload_choice + assert @jsmith.pref.respond_to?(:dmsf_attachments_upload_choice) + end + + def test_user_preference_has_default_dmsf_query + assert @jsmith.pref.respond_to?(:default_dmsf_query) + end + + def test_user_preference_has_receive_download_notification + assert @jsmith.pref.respond_to?(:receive_download_notification) + end +end diff --git a/test/unit_test.rb b/test/unit_test.rb new file mode 100644 index 00000000..57365f29 --- /dev/null +++ b/test/unit_test.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Daniel Munn , Karel Pičman +# +# 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 +# . + +module RedmineDmsf + module Test + # Unit test + class UnitTest < ActiveSupport::TestCase + def initialize(name) + super + # Load all plugin's fixtures + dir = File.join(File.dirname(__FILE__), 'fixtures') + ext = '.yml' + Dir.glob("#{dir}/**/*#{ext}").each do |file| + fixture = File.basename(file, ext) + ActiveRecord::FixtureSet.create_fixtures dir, fixture + end + end + + def setup + @admin = User.find_by(login: 'admin') + @jsmith = User.find_by(login: 'jsmith') + @dlopper = User.find_by(login: 'dlopper') + @manager_role = Role.find_by(name: 'Manager') + @developer_role = Role.find_by(name: 'Developer') + [@manager_role, @developer_role].each do |role| + role.add_permission! :view_dmsf_folders + role.add_permission! :folder_manipulation + role.add_permission! :view_dmsf_files + role.add_permission! :file_manipulation + role.add_permission! :file_delete + end + @project1 = Project.find 1 + @project2 = Project.find 2 + @project3 = Project.find 3 + @project5 = Project.find 5 + [@project1, @project2, @project3, @project5].each do |project| + project.enable_module! :dmsf + end + @file1 = DmsfFile.find 1 + @file2 = DmsfFile.find 2 + @file4 = DmsfFile.find 4 + @file5 = DmsfFile.find 5 + @file7 = DmsfFile.find 7 + @file8 = DmsfFile.find 8 + @file11 = DmsfFile.find 11 + @file13 = DmsfFile.find 13 + @folder_link1 = DmsfLink.find 1 + @file_link2 = DmsfLink.find 2 + @file_link7 = DmsfLink.find 7 + @folder1 = DmsfFolder.find 1 + @folder2 = DmsfFolder.find 2 + @folder6 = DmsfFolder.find 6 + @folder7 = DmsfFolder.find 7 + @folder8 = DmsfFolder.find 8 + Setting.plugin_redmine_dmsf['dmsf_storage_directory'] = File.join('files', ['dmsf']) + FileUtils.cp_r File.join(File.expand_path('../fixtures/files', __FILE__), '.'), DmsfFile.storage_path + User.current = nil + end + + def teardown + # Delete our tmp folder + FileUtils.rm_rf DmsfFile.storage_path + rescue StandardError => e + Rails.logger.error e.message + end + + protected + + def last_email + ActionMailer::Base.deliveries.last + end + + def text_part(email) + email.parts.detect { |part| part.content_type.include?('text/plain') } + end + + def html_part(email) + email.parts.detect { |part| part.content_type.include?('text/html') } + end + end + end +end