Compare commits

...

128 Commits

Author SHA1 Message Date
Roberto Rosario
e9989bac01 Merge remote-tracking branch 'origin/features/multi_version_document' into clients/bc 2019-10-11 12:39:18 -04:00
Roberto Rosario
4fe6b36069 Add OCR migration dependency
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 12:38:31 -04:00
Roberto Rosario
24cf9dcb0f Merge remote-tracking branch 'origin/features/disable_simple_search' into clients/bc 2019-10-11 10:53:50 -04:00
Roberto Rosario
9b7f133249 Support simple search disabling
Add new new SEARCH_DISABLE_SIMPLE_SEARCH setting.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 10:53:18 -04:00
Roberto Rosario
5227e196d0 Merge remote-tracking branch 'origin/features/multi_version_document' into clients/bc 2019-10-11 10:24:09 -04:00
Roberto Rosario
acd8fd2a3e PEP8 cleanup
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 10:22:42 -04:00
Roberto Rosario
beb3b936a6 Remove the documents app settings
Remove DOCUMENTS_DISABLE_BASE_IMAGE_CACHE,
DOCUMENTS_DISABLE_TRANSFORMED_IMAGE_CACHE, and
DOCUMENTS_FIX_ORIENTATION settings.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 10:22:09 -04:00
Roberto Rosario
f10cc89847 Add document page append view tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 10:10:04 -04:00
Roberto Rosario
b98807b336 Merge remote-tracking branch 'origin/features/multi_version_document' into clients/bc 2019-10-11 09:10:23 -04:00
Roberto Rosario
01e79b1089 Add OCR migration dependency
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 09:08:51 -04:00
Roberto Rosario
5ea286d4bd Show page append link if new versions are allowed
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 09:07:48 -04:00
Roberto Rosario
21bda59787 Merge remote-tracking branch 'origin/versions/minor' into clients/bc 2019-10-10 17:19:11 -04:00
Roberto Rosario
d865c60091 Update changelog
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 17:18:12 -04:00
Roberto Rosario
4afe81f306 Update document version upload to use dropzone
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 17:16:16 -04:00
Roberto Rosario
126dcfd609 Split source multiform template
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 17:15:45 -04:00
Roberto Rosario
0d209b33cb Merge remote-tracking branch 'origin/features/multi_version_document' into clients/bc 2019-10-10 16:33:09 -04:00
Roberto Rosario
77e3847025 Add directional migration dependency
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 16:32:27 -04:00
Roberto Rosario
5bf86c82e2 Fix base class name
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 15:53:00 -04:00
Roberto Rosario
4f93beae74 Fix base class name
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 15:52:35 -04:00
Roberto Rosario
b83c14bd36 Merge remote-tracking branch 'origin/features/multi_version_document' into clients/bc
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 15:03:41 -04:00
Roberto Rosario
af1eae8c52 Merge branch 'versions/minor' into features/multi_version_document
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 14:56:47 -04:00
Roberto Rosario
8aa5c31431 Update setup.py
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 14:09:53 -04:00
Roberto Rosario
9d13fdd9ce Generate requirements file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 12:17:38 -04:00
Roberto Rosario
bba956a65e Add missing literal import
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 12:01:10 -04:00
Roberto Rosario
0699ad0556 Add support for new document page structure
Documents now have their own dedicated DocumentPage
submodel. The old DocumentPage is now called DocumentVersionPage.
This allows mappings between document pages and document version
pages, allowing renumbering, appending pages.
DocumentPages have a content_object to map them to any other
object. For now they only map to DocumentVersionPages.
New option added to the version upload form to append the
pages of the new version.
A new view was added to just append new pages with wraps the
new document version upload form and hides the append pages
checkbox set to True.
Add a new action, reset_pages to reset the pages of the
document to those of the latest version.

Missing: appending tests, checks for proper content_object in OCR and
document parsing.

Author: Roberto Rosario <roberto.rosario@mayan-edms.com>
Date:   Thu Oct 11 12:00:25 2019 -0400
2019-10-10 11:55:42 -04:00
Roberto Rosario
739d496799 Add dedicated pages append action
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 11:53:40 -04:00
Roberto Rosario
ff03ea07ca Add support for appending pages
Add version upload form checkbox.
Add the append_pages keyword argument.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 02:40:45 -04:00
Roberto Rosario
03379ab8ec Fix parsing tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 01:14:46 -04:00
Roberto Rosario
a4a12b0cfe Fix tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 21:09:36 -04:00
Roberto Rosario
cf697d3ea7 Fix tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 21:06:38 -04:00
Roberto Rosario
a9077cb47a Fix document search tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 19:40:08 -04:00
Roberto Rosario
f163dc78d4 Fix search setup
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 17:15:59 -04:00
Roberto Rosario
64abf66f22 Fix tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 16:55:02 -04:00
Roberto Rosario
7fbb94a8ae Migration updates
Squash version page migrations.
Add manual OCR and parsing migrations.
Fix tests.
Page search updates.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 16:38:00 -04:00
Roberto Rosario
d0ee8aba16 Add document pages reset view
Add document version page count update view.
Add tests.
Register permission_document_tools to the Document model.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 11:53:29 -04:00
Roberto Rosario
5b37c7715d Fix document page render
Solve page_number > 1 error.
Add page_all to Document model.
Enable redactions.
Remove unused methods.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 00:38:08 -04:00
Roberto Rosario
8cf807899a Initial commit to support page mapping
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 18:45:53 -04:00
Roberto Rosario
4a99a9df3e Update run_test Docker command name
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 15:14:16 -04:00
Roberto Rosario
f0ca92c06b Update setup.py file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 10:31:30 -04:00
Roberto Rosario
653f55f84a Invalidate the layer cache in tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 09:44:39 -04:00
Roberto Rosario
9cf1d44ee7 Move Makefile versions to variables
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 09:44:33 -04:00
Roberto Rosario
69086d87dd Invalidate the layer cache in tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 09:43:10 -04:00
Roberto Rosario
89f05aeaa1 Move Makefile versions to variables
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 09:33:39 -04:00
Roberto Rosario
a966d6c8cf Add missing dependencies import
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 16:42:29 -04:00
Roberto Rosario
cf18e99caa Use virtualenv for CI
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 16:21:21 -04:00
Roberto Rosario
f87454c0b6 Fix Python3 pip install
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 14:50:21 -04:00
Roberto Rosario
b7febc8df5 Switch CI Python to 3
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 14:39:47 -04:00
Roberto Rosario
f4b34bf48d Fix importer for Python 3
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 14:04:01 -04:00
Roberto Rosario
d2fd865b68 Fix failing tests imports
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 11:28:34 -04:00
Roberto Rosario
a5e00ceba9 Remove conflicting migrations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 11:16:18 -04:00
Roberto Rosario
f09fec0aff Fix pending errors of the vendors/bc 33 merge
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 11:08:50 -04:00
Roberto Rosario
ce7c805251 Merge branch 'versions/minor' into clients/bc_33
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 11:01:43 -04:00
Roberto Rosario
1bcc9332b2 Merge remote-tracking branch 'origin/versions/micro' into client_bc_merge_micro
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 10:03:03 -04:00
Roberto Rosario
e3267d3973 Merge branch 'versions/minor' into bc_merge
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-05 01:06:25 -04:00
Roberto Rosario
eb7cbc73ee Merge branch 'features/weblinks' into clients/bc 2019-07-01 15:44:03 -04:00
Roberto Rosario
596b5ccf67 MVP of the weblinks app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 15:43:15 -04:00
Roberto Rosario
34838a438d Merge branch 'features/workflow_context' into clients/bc
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 10:01:15 -04:00
Roberto Rosario
572690e2bc Finish workflow context implementation
Improve workflow instance detail view.
Add workflow transition field widget support.
Fix workflow transition field required support.
Update tests.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 09:55:58 -04:00
Roberto Rosario
303e34299a Add a JSON and YAML validator to the common app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 09:41:45 -04:00
Roberto Rosario
c628de9ede Improve appearance of the object error list view
Add icon to the object error list link.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 09:41:06 -04:00
Roberto Rosario
e73be6bbab Don't error out if the settings are set to blank
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 01:12:31 -04:00
Roberto Rosario
c9fd8b02e3 Add field type selection
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-01 01:12:02 -04:00
Roberto Rosario
e1a63064dc Proof of concept of the workflow instance context
Add support for workflow instance JSON context.
Add support for two step workflow transition.
Add support for dynamic form creation for transition execution.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-30 09:51:22 -04:00
Roberto Rosario
42db8255d1 Merge branch 'versions/minor' into features/workflow_context
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-29 20:35:25 -04:00
Roberto Rosario
22ba6cfb49 Improve email metadata support
The feature can now work on emails with nested parts.
Also the metadata.yaml attachment no longer needs to be the
first attachment.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-29 02:13:51 -04:00
Roberto Rosario
02bba73ca7 Reduce code used to set bulk metadata
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-29 02:13:45 -04:00
Roberto Rosario
d0daf559c7 Remove the INSTALLED_APPS setting
The INSTALLED APPS setting is now replaced by the
new COMMON_EXTRA_APPS and COMMON_DISABLED_APPS.

Exposing the INSTALLED_APPS setting had the side effect
of blocking new apps that were added in new versions.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 23:52:46 -04:00
Roberto Rosario
f8f6700459 Add redirection after trashing a document
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 23:52:31 -04:00
Roberto Rosario
c318d37445 Fix IMAP4 store flags argument, GitLab issue #606
Python's documentation is incorrect, argument name is flag_list.
Closes GitLab issue #606. Thanks to Samuel Aebi (@samuelaebi)
for the report and debug information.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 23:50:53 -04:00
Roberto Rosario
246fc15988 Merge branch 'features/workflow_context' into clients/bc 2019-06-28 15:50:02 -04:00
Roberto Rosario
14d45cbe90 Use polylines for the edge splines
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:48:44 -04:00
Roberto Rosario
42a544c6e3 Merge branch 'features/workflow_context' into clients/bc 2019-06-28 15:37:21 -04:00
Roberto Rosario
75be11bc96 Hightlight initial state
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:35:33 -04:00
Roberto Rosario
ebf29d0eed Add actions to workflow preview
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 15:35:27 -04:00
Roberto Rosario
a391d27b44 Add transition form comment help text
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 14:33:37 -04:00
Roberto Rosario
753c9b8b4b Merge branch 'versions/minor' into features/workflow_context 2019-06-28 14:08:58 -04:00
Roberto Rosario
9cb6c6599d Create system user after migration
Move the code to trigger on the post_migrate signal.
Avoid "database not ready" errors during tests and initialsetup.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-28 13:59:00 -04:00
Roberto Rosario
8bd0d0166b Merge branch 'versions/minor' into clients/bc 2019-06-28 13:24:27 -04:00
Roberto Rosario
bee0c0b189 Add authentication events
Add event to track failed logins, password reset starts, and password
reset completions.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 17:04:44 -04:00
Roberto Rosario
744bfefa5c Add workflow email action template support
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 12:10:31 -04:00
Roberto Rosario
850fb16c8c Add automatic execution test
Add test for automatic email action execution on document upload.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 11:51:21 -04:00
Roberto Rosario
72ba805fbb Add test case database connection check
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 11:35:58 -04:00
Roberto Rosario
3d7b40f029 Add email action tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 09:54:48 -04:00
Roberto Rosario
2039a9f13b Merge branch 'clients/bc' into features/workflow_email_action 2019-06-27 08:45:27 -04:00
Roberto Rosario
bb8f12dd7a Update CHANGES file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:40:43 -04:00
Roberto Rosario
40ab1f3665 [FIX] Remove tag create document registration
Make no sense to have the tag create event register to existing tags.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:39:48 -04:00
Roberto Rosario
fdef757fd0 Add redactions app JavaScript dependencies
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:22:53 -04:00
Roberto Rosario
3608ee1141 Remove included cropper.js files
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 08:17:50 -04:00
Roberto Rosario
7fb3d61dff [Fix] Change to relative imports
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 07:03:55 -04:00
Roberto Rosario
e9aa11673b Initial commit of the workflow mail action
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-27 07:03:31 -04:00
Roberto Rosario
03a7aa5daf Add missing migrations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 15:04:30 -04:00
Roberto Rosario
755f20c5c4 Fix importer logging
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:20:00 -04:00
Roberto Rosario
64772e2e90 Update changes file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:18:29 -04:00
Roberto Rosario
75a4a426e0 Remove duplicated trashed document preview
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:18:11 -04:00
Roberto Rosario
42a7ebeea2 Finish redactions app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:16:11 -04:00
Roberto Rosario
3d22f48555 Add draw box by percentage
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:13:20 -04:00
Roberto Rosario
488e048d8f Remove old remarks and add redirect
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:12:41 -04:00
Roberto Rosario
2f82559a5c Add verbose name for the Redaction model
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:12:08 -04:00
Roberto Rosario
7d5b7b9fc4 Fix static media folder
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:11:52 -04:00
Roberto Rosario
7aa68b8bbf Initial commit of the redactions app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 12:13:49 -04:00
Roberto Rosario
aecde926f2 Fix varaible typo
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 12:08:25 -04:00
Roberto Rosario
6b95628e56 Add rectangle drawing transformation
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 10:23:30 -04:00
Roberto Rosario
56a1b97b46 Update changes file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 09:17:01 -04:00
Roberto Rosario
34a5a54c8b Add sortable index instance label column
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 09:15:52 -04:00
Roberto Rosario
0c17ab3f8a Improve source column exclusion
Improve for model subclasses in partial querysets.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 09:08:02 -04:00
Roberto Rosario
c967a25f82 Support exclusions from source columns
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 00:16:29 -04:00
Roberto Rosario
7562588c42 Fix typo
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:55:02 -04:00
Roberto Rosario
a1a706b7b9 Add link to sort individual indexes
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:50:01 -04:00
Roberto Rosario
d623cb2df5 Sort function
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:04:49 -04:00
Roberto Rosario
488ddcf1e1 Rename CHANGES file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:04:07 -04:00
Roberto Rosario
3d39893f17 Add columns to show document count per workflow
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:03:06 -04:00
Roberto Rosario
3694839d97 Use Select2 for the document type selection form
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 19:29:12 -04:00
Roberto Rosario
cce27aceca Allow client builds
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 16:42:59 -04:00
Roberto Rosario
c73d251370 Generate metadata by name not label
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 16:32:14 -04:00
Roberto Rosario
091f0d1cfd Generate new metadata when label is ambiguous
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 16:20:56 -04:00
Roberto Rosario
d2affdcf21 Merge branch 'feature/document_importer' into nightly
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 17:39:19 -04:00
Roberto Rosario
885d430b98 Merge branch 'versions/minor' into nightly 2019-06-21 17:38:08 -04:00
Roberto Rosario
39eabe1c54 Associate metadata to all types
Previously metadata types were associated to documents types
if the metadata type was newly created.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 17:37:00 -04:00
Roberto Rosario
f6ad579829 Merge branch 'versions/minor' into nightly
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 12:05:19 -04:00
Roberto Rosario
6fc9e46882 Merge branch 'versions/minor' into feature/document_importer 2019-06-21 11:53:09 -04:00
Roberto Rosario
2d326a679d Merge branch 'master' into feature/document_importer 2019-06-21 11:53:03 -04:00
Roberto Rosario
aa8c2db446 Merge branch 'master' into feature/document_importer
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 00:06:49 -04:00
Roberto Rosario
925b55d76d Support ignoring certain rows
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-20 10:12:53 -04:00
Roberto Rosario
5808d3653d Add support for ignoring import errors
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-20 10:05:24 -04:00
Roberto Rosario
bc072f7b7e Add column mapping support
Add support for specifying metadata columns.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 17:47:32 -04:00
Roberto Rosario
b3d59eee39 Add MVP of the importer app
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 16:02:00 -04:00
Roberto Rosario
7d379a52af Add a reusable task to upload documents
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 16:00:59 -04:00
Roberto Rosario
499ab1f3e7 Allow disabling the random primary key test mixin
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 15:59:15 -04:00
137 changed files with 3459 additions and 738 deletions

View File

@@ -63,6 +63,7 @@ job_docker_nightly:
only: only:
- nightly - nightly
- staging - staging
- /^clients\/.+$/
job_documentation_build: job_documentation_build:
stage: build_documentation stage: build_documentation
@@ -162,6 +163,7 @@ job_push_python:
- releases/python - releases/python
- staging - staging
- nightly - nightly
- /^clients\/.+$/
test-mysql: test-mysql:
<<: *test_base <<: *test_base

20
CHANGES_BC.rst Normal file
View File

@@ -0,0 +1,20 @@
- Use Select2 widget for the document type selection form.
- Update source column matching to be additive and not exclusive.
- Add two columns to show the number of documents per workflow and
workflow state.
- Sort module.
- Add link to sort individual indexes.
- Support exclusions from source columns.
- Improve source column exclusion. Improve for model subclasses in partial querysets.
- Add sortable index instance label column.
- Add rectangle drawing transformation.
- Redactions app.
- Remove duplicated trashed document preview.
- Add label to trashed date and time document source column.
- Tag created event fix.
3.2.3 (2019-06-21)
* Add a reusable task to upload documents.
* Add MVP of the importer app.
3.2.4-3.2.8 (2019-10-07)

View File

@@ -80,6 +80,11 @@
Deploy a Redis container. Deploy a Redis container.
- Improve document version upload form. - Improve document version upload form.
- Use dropzone for document version upload form. - Use dropzone for document version upload form.
- Remove the DOCUMENTS_DISABLE_BASE_IMAGE_CACHE,
DOCUMENTS_DISABLE_TRANSFORMED_IMAGE_CACHE, and
DOCUMENTS_FIX_ORIENTATION settings.
- Support simple search disable via the new
SEARCH_DISABLE_SIMPLE_SEARCH setting.
3.2.8 (2019-10-01) 3.2.8 (2019-10-01)
================== ==================

View File

@@ -1 +1 @@
3.3beta1 3.3beta1-bc

View File

@@ -19,7 +19,6 @@ Changes
GitLab issue #625. Thanks to Jesaja Everling (@jeverling) GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
for the report and the research. for the report and the research.
Removals Removals
-------- --------

View File

@@ -14,6 +14,7 @@ Changes
incorrectly state it is named flag_list. Closes GitLab issue incorrectly state it is named flag_list. Closes GitLab issue
#606. Thanks to Samuel Aebi (@samuelaebi) for the report and #606. Thanks to Samuel Aebi (@samuelaebi) for the report and
debug information. debug information.
debug information.
- Support configurable GUnicorn timeouts. Defaults to - Support configurable GUnicorn timeouts. Defaults to
current value of 120 seconds. current value of 120 seconds.
- Fix help text of the platformtemplate command. - Fix help text of the platformtemplate command.

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
__title__ = 'Mayan EDMS' __title__ = 'Mayan EDMS'
__version__ = '3.3beta1' __version__ = '3.3beta1'
__build__ = 0x030300 __build__ = 0x030300
__build_string__ = 'v3.3beta1-9-g1b327b99f0_Tue Oct 8 15:15:08 2019 -0400' __build_string__ = 'v3.3beta1-260-g9d13fdd9ce_Thu Oct 10 12:17:38 2019 -0400'
__django_version__ = '1.11' __django_version__ = '1.11'
__author__ = 'Roberto Rosario' __author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com' __author_email__ = 'roberto.rosario@mayan-edms.com'

View File

@@ -0,0 +1,19 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.events.classes import EventTypeNamespace
namespace = EventTypeNamespace(
label=_('Authentication'), name='authentication'
)
event_user_authentication_error = namespace.add_event_type(
label=_('User authentication error'), name='user_authentication_error'
)
event_user_password_reset_started = namespace.add_event_type(
label=_('User password reset started'), name='user_password_reset_started'
)
event_user_password_reset_complete = namespace.add_event_type(
label=_('User password reset complete'), name='user_password_reset_complete'
)

View File

@@ -0,0 +1,82 @@
from __future__ import unicode_literals
from django.conf import settings
from django.contrib.auth.views import (
INTERNAL_RESET_SESSION_TOKEN, INTERNAL_RESET_URL_TOKEN,
)
from django.core import mail
from actstream.models import Action
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.events.utils import create_system_user
from ..events import (
event_user_authentication_error, event_user_password_reset_complete,
event_user_password_reset_started
)
class AuthenticationEventsTestCase(GenericViewTestCase):
auto_login_user = False
def setUp(self):
super(AuthenticationEventsTestCase, self).setUp()
create_system_user()
def test_user_authentication_failure_event(self):
Action.objects.all().delete()
response = self.post(viewname=settings.LOGIN_URL)
self.assertEqual(response.status_code, 200)
action = Action.objects.last()
self.assertEqual(action.verb, event_user_authentication_error.id)
def test_user_password_reset_started_event(self):
Action.objects.all().delete()
response = self.post(
viewname='authentication:password_reset_view', data={
'email': self._test_case_user.email,
}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
action = Action.objects.last()
self.assertEqual(action.verb, event_user_password_reset_started.id)
def test_user_password_reset_complete_event(self):
response = self.post(
viewname='authentication:password_reset_view', data={
'email': self._test_case_user.email,
}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
email_parts = mail.outbox[0].body.replace('\n', '').split('/')
uidb64 = email_parts[-3]
token = email_parts[-2]
# Add the token to the session
session = self.client.session
session[INTERNAL_RESET_SESSION_TOKEN] = token
session.save()
Action.objects.all().delete()
new_password = 'new_password_123'
response = self.post(
viewname='authentication:password_reset_confirm_view',
kwargs={'uidb64': uidb64, 'token': INTERNAL_RESET_URL_TOKEN}, data={
'new_password1': new_password,
'new_password2': new_password
}
)
self.assertNotIn(INTERNAL_RESET_SESSION_TOKEN, self.client.session)
action = Action.objects.last()
self.assertEqual(action.verb, event_user_password_reset_complete.id)

View File

@@ -21,8 +21,13 @@ from mayan.apps.common.generics import MultipleObjectFormActionView
from mayan.apps.common.settings import ( from mayan.apps.common.settings import (
setting_home_view, setting_project_title, setting_project_url setting_home_view, setting_project_title, setting_project_url
) )
from mayan.apps.events.utils import get_system_user
from mayan.apps.user_management.permissions import permission_user_edit from mayan.apps.user_management.permissions import permission_user_edit
from .events import (
event_user_authentication_error, event_user_password_reset_complete,
event_user_password_reset_started
)
from .forms import EmailAuthenticationForm, UsernameAuthenticationForm from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
from .settings import setting_login_method, setting_maximum_session_length from .settings import setting_login_method, setting_maximum_session_length
@@ -57,6 +62,10 @@ class MayanLoginView(StrongholdPublicMixin, LoginView):
return result return result
def form_invalid(self, form):
event_user_authentication_error.commit(actor=get_system_user())
return super(MayanLoginView, self).form_invalid(form=form)
def get_form_class(self): def get_form_class(self):
if setting_login_method.value == 'email': if setting_login_method.value == 'email':
return EmailAuthenticationForm return EmailAuthenticationForm
@@ -112,6 +121,10 @@ class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmV
) )
template_name = 'authentication/password_reset_confirm.html' template_name = 'authentication/password_reset_confirm.html'
def post(self, *args, **kwargs):
event_user_password_reset_complete.commit(actor=get_system_user())
return super(MayanPasswordResetConfirmView, self).post(*args, **kwargs)
class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView): class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
extra_context = { extra_context = {
@@ -137,6 +150,10 @@ class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
) )
template_name = 'authentication/password_reset_form.html' template_name = 'authentication/password_reset_form.html'
def post(self, *args, **kwargs):
event_user_password_reset_started.commit(actor=get_system_user())
return super(MayanPasswordResetView, self).post(*args, **kwargs)
class UserSetPasswordView(MultipleObjectFormActionView): class UserSetPasswordView(MultipleObjectFormActionView):
form_class = SetPasswordForm form_class = SetPasswordForm

View File

@@ -10,12 +10,14 @@ from mayan.apps.common.menus import (
menu_facet, menu_list_facet, menu_main, menu_multi_item, menu_object, menu_facet, menu_list_facet, menu_main, menu_multi_item, menu_object,
menu_secondary menu_secondary
) )
from mayan.apps.documents.search import (
document_page_search, document_search, document_version_page_search
)
from mayan.apps.events.classes import ModelEventType from mayan.apps.events.classes import ModelEventType
from mayan.apps.events.links import ( from mayan.apps.events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list, link_events_for_object, link_object_event_types_user_subcriptions_list,
) )
from mayan.apps.events.permissions import permission_events_view from mayan.apps.events.permissions import permission_events_view
from mayan.apps.documents.search import document_page_search, document_search
from mayan.apps.navigation.classes import SourceColumn from mayan.apps.navigation.classes import SourceColumn
from .dependencies import * # NOQA from .dependencies import * # NOQA
@@ -115,12 +117,16 @@ class CabinetsApp(MayanAppConfig):
) )
document_page_search.add_model_field( document_page_search.add_model_field(
field='document_version__document__cabinets__label', field='document__cabinets__label',
label=_('Cabinets') label=_('Cabinets')
) )
document_search.add_model_field( document_search.add_model_field(
field='cabinets__label', label=_('Cabinets') field='cabinets__label', label=_('Cabinets')
) )
document_version_page_search.add_model_field(
field='document_version__document__cabinets__label',
label=_('Cabinets')
)
menu_facet.bind_links( menu_facet.bind_links(
links=(link_document_cabinet_list,), sources=(Document,) links=(link_document_cabinet_list,), sources=(Document,)

View File

@@ -8,7 +8,9 @@ from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.menus import ( from mayan.apps.common.menus import (
menu_facet, menu_list_facet, menu_object, menu_secondary menu_facet, menu_list_facet, menu_object, menu_secondary
) )
from mayan.apps.documents.search import document_page_search, document_search from mayan.apps.documents.search import (
document_page_search, document_search, document_version_page_search
)
from mayan.apps.events.classes import ModelEventType from mayan.apps.events.classes import ModelEventType
from mayan.apps.events.links import ( from mayan.apps.events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list link_events_for_object, link_object_event_types_user_subcriptions_list
@@ -80,13 +82,17 @@ class DocumentCommentsApp(MayanAppConfig):
SourceColumn(attribute='comment', source=Comment) SourceColumn(attribute='comment', source=Comment)
document_page_search.add_model_field( document_page_search.add_model_field(
field='document_version__document__comments__comment', field='document__comments__comment',
label=_('Comments') label=_('Comments')
) )
document_search.add_model_field( document_search.add_model_field(
field='comments__comment', field='comments__comment',
label=_('Comments') label=_('Comments')
) )
document_version_page_search.add_model_field(
field='document_version__document__comments__comment',
label=_('Comments')
)
menu_facet.bind_links( menu_facet.bind_links(
links=(link_comments_for_document,), sources=(Document,) links=(link_comments_for_document,), sources=(Document,)

View File

@@ -199,3 +199,36 @@ class IndexToolsViewTestCase(
# An instance root exists # An instance root exists
self.assertTrue(self.test_index.instance_root.pk) self.assertTrue(self.test_index.instance_root.pk)
def test_index_rebuild_view_no_permission(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(IndexInstanceNode.objects.count(), 0)
def test_index_rebuild_view_with_access(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
self.grant_access(
obj=self.test_index,
permission=permission_document_indexing_rebuild
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(IndexInstanceNode.objects.count(), 0)

View File

@@ -3,13 +3,13 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
DocumentPageContent, DocumentVersionParseError DocumentVersionPageContent, DocumentVersionParseError
) )
@admin.register(DocumentPageContent) @admin.register(DocumentVersionPageContent)
class DocumentPageContentAdmin(admin.ModelAdmin): class DocumentVersionPageContentAdmin(admin.ModelAdmin):
list_display = ('document_page',) list_display = ('document_version_page',)
@admin.register(DocumentVersionParseError) @admin.register(DocumentVersionParseError)

View File

@@ -8,7 +8,7 @@ from rest_framework.response import Response
from mayan.apps.documents.models import Document from mayan.apps.documents.models import Document
from mayan.apps.rest_api.permissions import MayanPermission from mayan.apps.rest_api.permissions import MayanPermission
from .models import DocumentPageContent from .models import DocumentVersionPageContent
from .permissions import permission_content_view from .permissions import permission_content_view
from .serializers import DocumentPageContentSerializer from .serializers import DocumentPageContentSerializer
@@ -41,8 +41,8 @@ class APIDocumentPageContentView(generics.RetrieveAPIView):
try: try:
content = instance.content content = instance.content
except DocumentPageContent.DoesNotExist: except DocumentVersionPageContent.DoesNotExist:
content = DocumentPageContent.objects.none() content = DocumentVersionPageContent.objects.none()
serializer = self.get_serializer(content) serializer = self.get_serializer(content)
return Response(serializer.data) return Response(serializer.data)

View File

@@ -12,7 +12,9 @@ from mayan.apps.common.classes import ModelField
from mayan.apps.common.menus import ( from mayan.apps.common.menus import (
menu_facet, menu_list_facet, menu_multi_item, menu_secondary, menu_tools menu_facet, menu_list_facet, menu_multi_item, menu_secondary, menu_tools
) )
from mayan.apps.documents.search import document_search, document_page_search from mayan.apps.documents.search import (
document_search, document_page_search, document_version_page_search
)
from mayan.apps.documents.signals import post_version_upload from mayan.apps.documents.signals import post_version_upload
from mayan.apps.events.classes import ModelEventType from mayan.apps.events.classes import ModelEventType
from mayan.apps.navigation.classes import SourceColumn from mayan.apps.navigation.classes import SourceColumn
@@ -43,7 +45,7 @@ from .permissions import (
permission_parse_document permission_parse_document
) )
from .signals import post_document_version_parsing from .signals import post_document_version_parsing
from .utils import get_document_content from .utils import get_document_content, get_document_version_content
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -74,6 +76,9 @@ class DocumentParsingApp(MayanAppConfig):
DocumentVersion = apps.get_model( DocumentVersion = apps.get_model(
app_label='documents', model_name='DocumentVersion' app_label='documents', model_name='DocumentVersion'
) )
DocumentVersionPage = apps.get_model(
app_label='documents', model_name='DocumentVersionPage'
)
DocumentVersionParseError = self.get_model( DocumentVersionParseError = self.get_model(
model_name='DocumentVersionParseError' model_name='DocumentVersionParseError'
) )
@@ -85,7 +90,7 @@ class DocumentParsingApp(MayanAppConfig):
name='content', value=get_document_content name='content', value=get_document_content
) )
DocumentVersion.add_to_class( DocumentVersion.add_to_class(
name='content', value=get_document_content name='content', value=get_document_version_content
) )
DocumentVersion.add_to_class( DocumentVersion.add_to_class(
name='submit_for_parsing', name='submit_for_parsing',
@@ -100,9 +105,9 @@ class DocumentParsingApp(MayanAppConfig):
) )
) )
ModelField( #ModelField(
model=Document, name='versions__version_pages__content__content' # model=Document, name='versions__pages__content__content'
) #)
ModelPermission.register( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
@@ -133,17 +138,17 @@ class DocumentParsingApp(MayanAppConfig):
) )
document_search.add_model_field( document_search.add_model_field(
field='versions__version_pages__content__content', label=_('Content') field='versions__pages__content__content', label=_('Content')
) )
document_page_search.add_model_field( document_version_page_search.add_model_field(
field='content__content', label=_('Content') field='content__content', label=_('Content')
) )
menu_facet.bind_links( menu_facet.bind_links(
links=(link_document_content,), sources=(Document,) links=(link_document_content,), sources=(Document,)
) )
menu_facet.bind_links( menu_list_facet.bind_links(
links=(link_document_page_content,), sources=(DocumentPage,) links=(link_document_page_content,), sources=(DocumentPage,)
) )
menu_list_facet.bind_links( menu_list_facet.bind_links(

View File

@@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext
from mayan.apps.common.widgets import TextAreaDiv from mayan.apps.common.widgets import TextAreaDiv
from .models import DocumentPageContent from .models import DocumentVersionPageContent
class DocumentContentForm(forms.Form): class DocumentContentForm(forms.Form):
@@ -26,10 +26,10 @@ class DocumentContentForm(forms.Form):
except AttributeError: except AttributeError:
document_pages = [] document_pages = []
for page in document_pages: for document_page in document_pages:
try: try:
page_content = page.content.content page_content = document_page.content_object.content.content
except DocumentPageContent.DoesNotExist: except DocumentVersionPageContent.DoesNotExist:
pass pass
else: else:
content.append(conditional_escape(force_text(page_content))) content.append(conditional_escape(force_text(page_content)))
@@ -37,7 +37,7 @@ class DocumentContentForm(forms.Form):
'\n\n\n<hr/><div class="document-page-content-divider">- %s -</div><hr/>\n\n\n' % ( '\n\n\n<hr/><div class="document-page-content-divider">- %s -</div><hr/>\n\n\n' % (
ugettext( ugettext(
'Page %(page_number)d' 'Page %(page_number)d'
) % {'page_number': page.page_number} ) % {'page_number': document_page.page_number}
) )
) )
@@ -72,8 +72,8 @@ class DocumentPageContentForm(forms.Form):
self.fields['contents'].initial = '' self.fields['contents'].initial = ''
try: try:
page_content = document_page.content.content page_content = document_page.content_object.content.content
except DocumentPageContent.DoesNotExist: except DocumentVersionPageContent.DoesNotExist:
pass pass
else: else:
content = conditional_escape(force_text(page_content)) content = conditional_escape(force_text(page_content))

View File

@@ -17,6 +17,9 @@ icon_document_content_download = Icon(
icon_document_multiple_submit = Icon( icon_document_multiple_submit = Icon(
driver_name='fontawesome', symbol='font' driver_name='fontawesome', symbol='font'
) )
icon_document_page_content = Icon(
driver_name='fontawesome', symbol='font'
)
icon_document_submit = Icon( icon_document_submit = Icon(
driver_name='fontawesome', symbol='font' driver_name='fontawesome', symbol='font'
) )

View File

@@ -32,9 +32,15 @@ link_document_content_delete_multiple = Link(
text=_('Delete parsed content'), text=_('Delete parsed content'),
view='document_parsing:document_content_delete_multiple', view='document_parsing:document_content_delete_multiple',
) )
link_document_content_download = Link(
args='resolved_object.id',
icon_class_path='mayan.apps.document_parsing.icons.icon_document_content_download',
permissions=(permission_content_view,), text=_('Download content'),
view='document_parsing:document_content_download'
)
link_document_page_content = Link( link_document_page_content = Link(
args='resolved_object.id', conditional_disable=is_document_page_disabled, args='resolved_object.id', conditional_disable=is_document_page_disabled,
icon_class_path='mayan.apps.document_parsing.icons.icon_document_content', icon_class_path='mayan.apps.document_parsing.icons.icon_document_page_content',
permissions=(permission_content_view,), text=_('Content'), permissions=(permission_content_view,), text=_('Content'),
view='document_parsing:document_page_content' view='document_parsing:document_page_content'
) )
@@ -44,12 +50,6 @@ link_document_parsing_errors_list = Link(
permissions=(permission_content_view,), text=_('Parsing errors'), permissions=(permission_content_view,), text=_('Parsing errors'),
view='document_parsing:document_parsing_error_list' view='document_parsing:document_parsing_error_list'
) )
link_document_content_download = Link(
args='resolved_object.id',
icon_class_path='mayan.apps.document_parsing.icons.icon_document_content_download',
permissions=(permission_content_view,), text=_('Download content'),
view='document_parsing:document_content_download'
)
link_document_submit_multiple = Link( link_document_submit_multiple = Link(
icon_class_path='mayan.apps.document_parsing.icons.icon_document_submit', icon_class_path='mayan.apps.document_parsing.icons.icon_document_submit',
text=_('Submit for parsing'), text=_('Submit for parsing'),

View File

@@ -18,11 +18,13 @@ from .signals import post_document_version_parsing
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DocumentPageContentManager(models.Manager): class DocumentVersionPageContentManager(models.Manager):
def delete_content_for(self, document, user=None): def delete_content_for(self, document, user=None):
with transaction.atomic(): with transaction.atomic():
for document_page in document.pages.all(): for document_page in document.pages.all():
self.filter(document_page=document_page).delete() self.filter(
document_version_page=document_page.content_object
).delete()
event_parsing_document_content_deleted.commit( event_parsing_document_content_deleted.commit(
actor=user, target=document actor=user, target=document

View File

@@ -12,6 +12,9 @@ class Migration(migrations.Migration):
('documents', '0041_auto_20170823_1855'), ('documents', '0041_auto_20170823_1855'),
] ]
run_before = [
('documents', '0052_rename_document_page'),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='DocumentPageContent', name='DocumentPageContent',

View File

@@ -0,0 +1,42 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('document_parsing', '0004_auto_20180917_0645'),
('documents', '0052_rename_document_page'),
]
operations = [
migrations.RenameModel(
'DocumentPageContent', 'DocumentVersionPageContent'
),
migrations.AlterField(
model_name='documentversionpagecontent',
name='document_page',
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name='content',
to='documents.DocumentVersionPage',
verbose_name='Document version page'
),
),
migrations.RenameField(
model_name='documentversionpagecontent',
old_name='document_page',
new_name='document_version_page',
),
migrations.AlterModelOptions(
name='documentversionpagecontent',
options={
'verbose_name': 'Document version page content',
'verbose_name_plural': 'Document version pages contents'
},
),
]

View File

@@ -5,36 +5,12 @@ from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.documents.models import ( from mayan.apps.documents.models import (
DocumentPage, DocumentType, DocumentVersion DocumentPage, DocumentType, DocumentVersion, DocumentVersionPage
) )
from .managers import DocumentPageContentManager, DocumentTypeSettingsManager from .managers import (
DocumentVersionPageContentManager, DocumentTypeSettingsManager
)
@python_2_unicode_compatible
class DocumentPageContent(models.Model):
"""
This model store's the parsed content of a document page.
"""
document_page = models.OneToOneField(
on_delete=models.CASCADE, related_name='content', to=DocumentPage,
verbose_name=_('Document page')
)
content = models.TextField(
blank=True, help_text=_(
'The actual text content as extracted by the document '
'parsing backend.'
), verbose_name=_('Content')
)
objects = DocumentPageContentManager()
class Meta:
verbose_name = _('Document page content')
verbose_name_plural = _('Document pages contents')
def __str__(self):
return force_text(self.document_page)
class DocumentTypeSettings(models.Model): class DocumentTypeSettings(models.Model):
@@ -62,6 +38,32 @@ class DocumentTypeSettings(models.Model):
verbose_name_plural = _('Document types settings') verbose_name_plural = _('Document types settings')
@python_2_unicode_compatible
class DocumentVersionPageContent(models.Model):
"""
This model store's the parsed content of a document page.
"""
document_version_page = models.OneToOneField(
on_delete=models.CASCADE, related_name='content',
to=DocumentVersionPage, verbose_name=_('Document version page')
)
content = models.TextField(
blank=True, help_text=_(
'The actual text content as extracted by the document '
'parsing backend.'
), verbose_name=_('Content')
)
objects = DocumentVersionPageContentManager()
class Meta:
verbose_name = _('Document version page content')
verbose_name_plural = _('Document version pages contents')
def __str__(self):
return force_text(self.document_page)
@python_2_unicode_compatible @python_2_unicode_compatible
class DocumentVersionParseError(models.Model): class DocumentVersionParseError(models.Model):
""" """

View File

@@ -23,11 +23,13 @@ class Parser(object):
_registry = {} _registry = {}
@classmethod @classmethod
def parse_document_page(cls, document_page): def parse_document_version_page(cls, document_version_page):
for parser_class in cls._registry.get(document_page.document_version.mimetype, ()): for parser_class in cls._registry.get(document_version_page.document_version.mimetype, ()):
try: try:
parser = parser_class() parser = parser_class()
parser.process_document_page(document_page) parser.process_document_page(
document_version_page=document_version_page
)
except ParserError: except ParserError:
# If parser raises error, try next parser in the list # If parser raises error, try next parser in the list
pass pass
@@ -41,7 +43,9 @@ class Parser(object):
for parser_class in cls._registry.get(document_version.mimetype, ()): for parser_class in cls._registry.get(document_version.mimetype, ()):
try: try:
parser = parser_class() parser = parser_class()
parser.process_document_version(document_version) parser.process_document_version(
document_version=document_version
)
except ParserError: except ParserError:
# If parser raises error, try next parser in the list # If parser raises error, try next parser in the list
pass pass
@@ -64,29 +68,33 @@ class Parser(object):
) )
logger.debug('document version: %d', document_version.pk) logger.debug('document version: %d', document_version.pk)
for document_page in document_version.pages.all(): for document_version_page in document_version.pages.all():
self.process_document_page(document_page=document_page) self.process_document_version_page(
document_version_page=document_version_page
)
def process_document_page(self, document_page): def process_document_version_page(self, document_version_page):
DocumentPageContent = apps.get_model( DocumentVersionPageContent = apps.get_model(
app_label='document_parsing', model_name='DocumentPageContent' app_label='document_parsing',
model_name='DocumentVersionPageContent'
) )
logger.info( logger.info(
'Processing page: %d of document version: %s', 'Processing page: %d of document version: %s',
document_page.page_number, document_page.document_version document_version_page.page_number,
document_version_page.document_version
) )
file_object = document_page.document_version.get_intermediate_file() file_object = document_version_page.document_version.get_intermediate_file()
try: try:
document_page_content, created = DocumentPageContent.objects.get_or_create( document_version_page_content, created = DocumentVersionPageContent.objects.get_or_create(
document_page=document_page document_version_page=document_version_page
) )
document_page_content.content = self.execute( document_version_page_content.content = self.execute(
file_object=file_object, page_number=document_page.page_number file_object=file_object, page_number=document_version_page.page_number
) )
document_page_content.save() document_version_page_content.save()
except Exception as exception: except Exception as exception:
error_message = _('Exception parsing page; %s') % exception error_message = _('Exception parsing page; %s') % exception
logger.error(error_message) logger.error(error_message)
@@ -96,7 +104,8 @@ class Parser(object):
logger.info( logger.info(
'Finished processing page: %d of document version: %s', 'Finished processing page: %d of document version: %s',
document_page.page_number, document_page.document_version document_version_page.page_number,
document_version_page.document_version
) )
def execute(self, file_object, page_number): def execute(self, file_object, page_number):

View File

@@ -2,10 +2,10 @@ from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from .models import DocumentPageContent from .models import DocumentVersionPageContent
class DocumentPageContentSerializer(serializers.ModelSerializer): class DocumentPageContentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
fields = ('content',) fields = ('content',)
model = DocumentPageContent model = DocumentVersionPageContent

View File

@@ -14,8 +14,8 @@ def task_parse_document_version(document_version_pk):
DocumentVersion = apps.get_model( DocumentVersion = apps.get_model(
app_label='documents', model_name='DocumentVersion' app_label='documents', model_name='DocumentVersion'
) )
DocumentPageContent = apps.get_model( DocumentVersionPageContent = apps.get_model(
app_label='document_parsing', model_name='DocumentPageContent' app_label='document_parsing', model_name='DocumentVersionPageContent'
) )
document_version = DocumentVersion.objects.get( document_version = DocumentVersion.objects.get(
@@ -24,6 +24,6 @@ def task_parse_document_version(document_version_pk):
logger.info( logger.info(
'Starting parsing for document version: %s', document_version 'Starting parsing for document version: %s', document_version
) )
DocumentPageContent.objects.process_document_version( DocumentVersionPageContent.objects.process_document_version(
document_version=document_version document_version=document_version
) )

View File

@@ -10,7 +10,7 @@ from ..events import (
event_parsing_document_version_submit, event_parsing_document_version_submit,
event_parsing_document_version_finish event_parsing_document_version_finish
) )
from ..models import DocumentPageContent from ..models import DocumentVersionPageContent
class DocumentParsingEventsTestCase(GenericDocumentTestCase): class DocumentParsingEventsTestCase(GenericDocumentTestCase):
@@ -19,7 +19,7 @@ class DocumentParsingEventsTestCase(GenericDocumentTestCase):
def test_document_content_deleted_event(self): def test_document_content_deleted_event(self):
Action.objects.all().delete() Action.objects.all().delete()
DocumentPageContent.objects.delete_content_for( DocumentVersionPageContent.objects.delete_content_for(
document=self.test_document document=self.test_document
) )

View File

@@ -18,5 +18,5 @@ class ParserTestCase(DocumentTestMixin, BaseTestCase):
parser.process_document_version(self.test_document.latest_version) parser.process_document_version(self.test_document.latest_version)
self.assertTrue( self.assertTrue(
TEST_DOCUMENT_CONTENT in self.test_document.pages.first().content.content TEST_DOCUMENT_CONTENT in self.test_document.pages.first().content_object.content.content
) )

View File

@@ -5,7 +5,7 @@ from django.test import override_settings
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from mayan.apps.documents.tests.literals import TEST_HYBRID_DOCUMENT from mayan.apps.documents.tests.literals import TEST_HYBRID_DOCUMENT
from ..models import DocumentPageContent from ..models import DocumentVersionPageContent
from ..permissions import ( from ..permissions import (
permission_content_view, permission_document_type_parsing_setup, permission_content_view, permission_document_type_parsing_setup,
permission_parse_document permission_parse_document
@@ -72,8 +72,8 @@ class DocumentContentViewsTestCase(
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertTrue( self.assertTrue(
DocumentPageContent.objects.filter( DocumentVersionPageContent.objects.filter(
document_page=self.test_document.pages.first() document_version_page=self.test_document.pages.first().content_object
).exists() ).exists()
) )
@@ -86,8 +86,8 @@ class DocumentContentViewsTestCase(
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertFalse( self.assertFalse(
DocumentPageContent.objects.filter( DocumentVersionPageContent.objects.filter(
document_page=self.test_document.pages.first() document_version_page=self.test_document.pages.first().content_object
).exists() ).exists()
) )

View File

@@ -7,7 +7,9 @@ from .views import (
DocumentContentView, DocumentContentDeleteView, DocumentContentView, DocumentContentDeleteView,
DocumentContentDownloadView, DocumentPageContentView, DocumentContentDownloadView, DocumentPageContentView,
DocumentParsingErrorsListView, DocumentSubmitView, DocumentParsingErrorsListView, DocumentSubmitView,
DocumentTypeSettingsEditView, DocumentTypeSubmitView, ParseErrorListView DocumentTypeSettingsEditView, DocumentTypeSubmitView,
DocumentVersionPageContentView,
ParseErrorListView
) )
urlpatterns = [ urlpatterns = [
@@ -34,6 +36,11 @@ urlpatterns = [
regex=r'^documents/pages/(?P<pk>\d+)/content/$', regex=r'^documents/pages/(?P<pk>\d+)/content/$',
view=DocumentPageContentView.as_view(), name='document_page_content' view=DocumentPageContentView.as_view(), name='document_page_content'
), ),
url(
regex=r'^documents/versions/pages/(?P<pk>\d+)/content/$',
view=DocumentVersionPageContentView.as_view(),
name='document_version_page_content'
),
url( url(
regex=r'^documents/(?P<pk>\d+)/submit/$', regex=r'^documents/(?P<pk>\d+)/submit/$',
view=DocumentSubmitView.as_view(), name='document_submit' view=DocumentSubmitView.as_view(), name='document_submit'

View File

@@ -6,14 +6,28 @@ from django.utils.html import conditional_escape
def get_document_content(document): def get_document_content(document):
DocumentPageContent = apps.get_model( DocumentVersionPageContent = apps.get_model(
app_label='document_parsing', model_name='DocumentPageContent' app_label='document_parsing', model_name='DocumentVersionPageContent'
) )
for page in document.pages.all(): for document_page in document.pages.all():
try: try:
page_content = page.content.content page_content = document_page.content_object.content.content
except DocumentPageContent.DoesNotExist: except DocumentVersionPageContent.DoesNotExist:
pass
else:
yield conditional_escape(force_text(page_content))
def get_document_version_content(document_version):
DocumentVersionPageContent = apps.get_model(
app_label='document_parsing', model_name='DocumentVersionPageContent'
)
for document_version_page in document_version.pages.all():
try:
page_content = document_version_page.content.content
except DocumentVersionPageContent.DoesNotExist:
pass pass
else: else:
yield conditional_escape(force_text(page_content)) yield conditional_escape(force_text(page_content))

View File

@@ -12,10 +12,12 @@ from mayan.apps.common.generics import (
) )
from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.forms import DocumentTypeFilteredSelectForm from mayan.apps.documents.forms import DocumentTypeFilteredSelectForm
from mayan.apps.documents.models import Document, DocumentPage, DocumentType from mayan.apps.documents.models import (
Document, DocumentPage, DocumentType, DocumentVersionPage
)
from .forms import DocumentContentForm, DocumentPageContentForm from .forms import DocumentContentForm, DocumentPageContentForm
from .models import DocumentPageContent, DocumentVersionParseError from .models import DocumentVersionPageContent, DocumentVersionParseError
from .permissions import ( from .permissions import (
permission_content_view, permission_document_type_parsing_setup, permission_content_view, permission_document_type_parsing_setup,
permission_parse_document permission_parse_document
@@ -46,7 +48,7 @@ class DocumentContentDeleteView(MultipleObjectConfirmActionView):
return result return result
def object_action(self, form, instance): def object_action(self, form, instance):
DocumentPageContent.objects.delete_content_for( DocumentVersionPageContent.objects.delete_content_for(
document=instance, user=self.request.user document=instance, user=self.request.user
) )
@@ -107,6 +109,30 @@ class DocumentPageContentView(SingleObjectDetailView):
} }
class DocumentVersionPageContentView(SingleObjectDetailView):
form_class = DocumentPageContentForm
model = DocumentVersionPage
object_permission = permission_content_view
def dispatch(self, request, *args, **kwargs):
result = super(DocumentPageContentView, self).dispatch(
request, *args, **kwargs
)
self.get_object().document.add_as_recent_document_for_user(
request.user
)
return result
def get_extra_context(self):
return {
'hide_labels': True,
'object': self.get_object(),
'title': _(
'Content for document version page: %s'
) % self.get_object(),
}
class DocumentParsingErrorsListView(SingleObjectListView): class DocumentParsingErrorsListView(SingleObjectListView):
view_permission = permission_content_view view_permission = permission_content_view

View File

@@ -205,10 +205,10 @@ class DocumentStatesApp(MayanAppConfig):
SourceColumn( SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Date and time'), source=WorkflowInstanceLogEntry, label=_('Date and time'),
attribute='datetime' attribute='datetime', is_sortable=True
) )
SourceColumn( SourceColumn(
source=WorkflowInstanceLogEntry, label=_('User'), attribute='user' source=WorkflowInstanceLogEntry, attribute='user', is_sortable=True
) )
SourceColumn( SourceColumn(
source=WorkflowInstanceLogEntry, source=WorkflowInstanceLogEntry,

View File

@@ -162,6 +162,7 @@ link_workflow_template_transition_field_delete = Link(
tags='dangerous', text=_('Delete'), tags='dangerous', text=_('Delete'),
view='document_states:workflow_template_transition_field_delete', view='document_states:workflow_template_transition_field_delete',
) )
link_workflow_template_transition_field_edit = Link( link_workflow_template_transition_field_edit = Link(
args='resolved_object.pk', args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_edit', icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_edit',

View File

@@ -6,6 +6,11 @@ import logging
from furl import furl from furl import furl
from graphviz import Digraph from graphviz import Digraph
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
@@ -328,8 +333,8 @@ class WorkflowState(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Solve issue #557 "Break workflows with invalid input" # Solve issue #557 "Break workflows with invalid input"
# without using a migration. # without using a migration.
# Remove blank=True, remove this, and create a migration in the next # TODO: Remove blank=True, remove this, and create a migration in the
# minor version. # next minor version.
try: try:
self.completion = int(self.completion) self.completion = int(self.completion)

View File

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from ..literals import FIELD_TYPE_CHOICE_CHAR
from ..models import WorkflowTransition from ..models import WorkflowTransition
from ..permissions import ( from ..permissions import (
permission_workflow_edit, permission_workflow_view, permission_workflow_edit, permission_workflow_view,
@@ -19,6 +20,11 @@ from .mixins import (
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin
) )
TEST_WORKFLOW_TRANSITION_FIELD_NAME = 'test_workflow_transition_field'
TEST_WORKFLOW_TRANSITION_FIELD_LABEL = 'test workflow transition field'
TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT = 'test workflow transition field help test'
TEST_WORKFLOW_TRANSITION_FIELD_TYPE = FIELD_TYPE_CHOICE_CHAR
class WorkflowTransitionViewTestCase( class WorkflowTransitionViewTestCase(
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin, WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin,

View File

@@ -3,18 +3,12 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
DeletedDocument, Document, DocumentPage, DocumentType, DeletedDocument, Document, DocumentType, DocumentTypeFilename,
DocumentTypeFilename, DocumentVersion, DuplicatedDocument, RecentDocument DocumentVersion, DocumentVersionPage, DuplicatedDocument,
RecentDocument
) )
class DocumentPageInline(admin.StackedInline):
model = DocumentPage
extra = 1
classes = ('collapse-open',)
allow_add = True
class DocumentTypeFilenameInline(admin.StackedInline): class DocumentTypeFilenameInline(admin.StackedInline):
model = DocumentTypeFilename model = DocumentTypeFilename
extra = 1 extra = 1
@@ -29,6 +23,13 @@ class DocumentVersionInline(admin.StackedInline):
allow_add = True allow_add = True
class DocumentVersionPageInline(admin.StackedInline):
model = DocumentVersionPage
extra = 1
classes = ('collapse-open',)
allow_add = True
@admin.register(DeletedDocument) @admin.register(DeletedDocument)
class DeletedDocumentAdmin(admin.ModelAdmin): class DeletedDocumentAdmin(admin.ModelAdmin):
date_hierarchy = 'deleted_date_time' date_hierarchy = 'deleted_date_time'

View File

@@ -33,10 +33,14 @@ from .serializers import (
DocumentTypeSerializer, DocumentVersionSerializer, DocumentTypeSerializer, DocumentVersionSerializer,
NewDocumentSerializer, NewDocumentVersionSerializer, NewDocumentSerializer, NewDocumentVersionSerializer,
RecentDocumentSerializer, WritableDocumentSerializer, RecentDocumentSerializer, WritableDocumentSerializer,
WritableDocumentTypeSerializer, WritableDocumentVersionSerializer WritableDocumentTypeSerializer, WritableDocumentVersionSerializer,
DocumentVersionPageSerializer
) )
from .settings import settings_document_page_image_cache_time from .settings import settings_document_page_image_cache_time
from .tasks import task_generate_document_page_image from .tasks import (
task_generate_document_page_image,
task_generate_document_version_page_image
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -168,13 +172,8 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
) )
return document return document
def get_document_version(self):
return get_object_or_404(
self.get_document().versions.all(), pk=self.kwargs['version_pk']
)
def get_queryset(self): def get_queryset(self):
return self.get_document_version().pages_all.all() return self.get_document().pages_all.all()
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
return None return None
@@ -221,6 +220,95 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
return response return response
class APIDocumentVersionPageImageView(generics.RetrieveAPIView):
"""
get: Returns an image representation of the selected document version page.
"""
lookup_url_kwarg = 'page_pk'
def get_document(self):
if self.request.method == 'GET':
permission_required = permission_document_view
else:
permission_required = permission_document_edit
document = get_object_or_404(Document.passthrough, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
obj=document, permissions=(permission_required,),
user=self.request.user
)
return document
def get_document_version(self):
return get_object_or_404(
self.get_document().versions.all(), pk=self.kwargs['version_pk']
)
def get_queryset(self):
return self.get_document_version().pages.all()
def get_serializer(self, *args, **kwargs):
return None
def get_serializer_class(self):
return None
@cache_control(private=True)
def retrieve(self, request, *args, **kwargs):
width = request.GET.get('width')
height = request.GET.get('height')
zoom = request.GET.get('zoom')
if zoom:
zoom = int(zoom)
rotation = request.GET.get('rotation')
if rotation:
rotation = int(rotation)
maximum_layer_order = request.GET.get('maximum_layer_order')
if maximum_layer_order:
maximum_layer_order = int(maximum_layer_order)
task = task_generate_document_version_page_image.apply_async(
kwargs=dict(
document_version_page_id=self.get_object().pk, width=width,
height=height, zoom=zoom, rotation=rotation,
maximum_layer_order=maximum_layer_order,
user_id=request.user.pk
)
)
cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT)
cache_file = self.get_object().cache_partition.get_file(filename=cache_filename)
with cache_file.open() as file_object:
response = HttpResponse(file_object.read(), content_type='image')
if '_hash' in request.GET:
patch_cache_control(
response=response,
max_age=settings_document_page_image_cache_time.value
)
return response
class APIDocumentPageListView(generics.ListAPIView):
serializer_class = DocumentPageSerializer
def get_document(self):
document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
obj=document, permissions=(permission_document_view,),
user=self.request.user
)
return document
def get_queryset(self):
return self.get_document().pages.all()
class APIDocumentPageView(generics.RetrieveUpdateAPIView): class APIDocumentPageView(generics.RetrieveUpdateAPIView):
""" """
get: Returns the selected document page details. get: Returns the selected document page details.
@@ -230,6 +318,33 @@ class APIDocumentPageView(generics.RetrieveUpdateAPIView):
lookup_url_kwarg = 'page_pk' lookup_url_kwarg = 'page_pk'
serializer_class = DocumentPageSerializer serializer_class = DocumentPageSerializer
def get_document(self):
if self.request.method == 'GET':
permission_required = permission_document_view
else:
permission_required = permission_document_edit
document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
obj=document, permissions=(permission_required,),
user=self.request.user
)
return document
def get_queryset(self):
return self.get_document().pages.all()
class APIDocumentVersionPageView(generics.RetrieveUpdateAPIView):
"""
get: Returns the selected document verion page details.
patch: Edit the selected document version page.
put: Edit the selected document version page.
"""
lookup_url_kwarg = 'page_pk'
serializer_class = DocumentVersionPageSerializer
def get_document(self): def get_document(self):
if self.request.method == 'GET': if self.request.method == 'GET':
permission_required = permission_document_view permission_required = permission_document_view
@@ -423,7 +538,7 @@ class APIRecentDocumentListView(generics.ListAPIView):
class APIDocumentVersionPageListView(generics.ListAPIView): class APIDocumentVersionPageListView(generics.ListAPIView):
serializer_class = DocumentPageSerializer serializer_class = DocumentVersionPageSerializer
def get_document(self): def get_document(self):
document = get_object_or_404(Document, pk=self.kwargs['pk']) document = get_object_or_404(Document, pk=self.kwargs['pk'])

View File

@@ -59,7 +59,7 @@ from .links import (
link_document_multiple_delete, link_document_multiple_document_type_edit, link_document_multiple_delete, link_document_multiple_document_type_edit,
link_document_multiple_download, link_document_multiple_favorites_add, link_document_multiple_download, link_document_multiple_favorites_add,
link_document_multiple_favorites_remove, link_document_multiple_restore, link_document_multiple_favorites_remove, link_document_multiple_restore,
link_document_multiple_trash, link_document_multiple_update_page_count, link_document_multiple_trash, link_document_multiple_pages_reset,
link_document_page_disable, link_document_page_multiple_disable, link_document_page_disable, link_document_page_multiple_disable,
link_document_page_enable, link_document_page_multiple_enable, link_document_page_enable, link_document_page_multiple_enable,
link_document_page_navigation_first, link_document_page_navigation_last, link_document_page_navigation_first, link_document_page_navigation_last,
@@ -74,8 +74,10 @@ from .links import (
link_document_type_filename_create, link_document_type_filename_delete, link_document_type_filename_create, link_document_type_filename_delete,
link_document_type_filename_edit, link_document_type_filename_list, link_document_type_filename_edit, link_document_type_filename_list,
link_document_type_list, link_document_type_policies, link_document_type_list, link_document_type_policies,
link_document_type_setup, link_document_update_page_count, link_document_type_setup, link_document_pages_reset,
link_document_version_download, link_document_version_list, link_document_version_download, link_document_version_list,
link_document_version_multiple_page_count_update,
link_document_version_page_count_update,
link_document_version_return_document, link_document_version_return_list, link_document_version_return_document, link_document_version_return_list,
link_document_version_revert, link_document_version_view, link_document_version_revert, link_document_version_view,
link_duplicated_document_list, link_duplicated_document_scan, link_duplicated_document_list, link_duplicated_document_scan,
@@ -87,10 +89,10 @@ from .permissions import (
permission_document_download, permission_document_edit, permission_document_download, permission_document_edit,
permission_document_new_version, permission_document_print, permission_document_new_version, permission_document_print,
permission_document_properties_edit, permission_document_restore, permission_document_properties_edit, permission_document_restore,
permission_document_trash, permission_document_type_delete, permission_document_tools, permission_document_trash,
permission_document_type_edit, permission_document_type_view, permission_document_type_delete, permission_document_type_edit,
permission_document_version_revert, permission_document_version_view, permission_document_type_view, permission_document_version_revert,
permission_document_view permission_document_version_view, permission_document_view,
) )
# Just import to initialize the search models # Just import to initialize the search models
from .search import document_search, document_page_search # NOQA from .search import document_search, document_page_search # NOQA
@@ -121,10 +123,11 @@ class DocumentsApp(MayanAppConfig):
DeletedDocument = self.get_model(model_name='DeletedDocument') DeletedDocument = self.get_model(model_name='DeletedDocument')
Document = self.get_model(model_name='Document') Document = self.get_model(model_name='Document')
DocumentPage = self.get_model(model_name='DocumentPage') DocumentPage = self.get_model(model_name='DocumentPage')
DocumentPageResult = self.get_model(model_name='DocumentPageResult') DocumentPageResult = self.get_model(model_name='DocumentVersionPageResult')
DocumentType = self.get_model(model_name='DocumentType') DocumentType = self.get_model(model_name='DocumentType')
DocumentTypeFilename = self.get_model(model_name='DocumentTypeFilename') DocumentTypeFilename = self.get_model(model_name='DocumentTypeFilename')
DocumentVersion = self.get_model(model_name='DocumentVersion') DocumentVersion = self.get_model(model_name='DocumentVersion')
DocumentVersionPage = self.get_model(model_name='DocumentVersionPage')
DuplicatedDocument = self.get_model(model_name='DuplicatedDocument') DuplicatedDocument = self.get_model(model_name='DuplicatedDocument')
DynamicSerializerField.add_serializer( DynamicSerializerField.add_serializer(
@@ -190,13 +193,15 @@ class DocumentsApp(MayanAppConfig):
permission_acl_edit, permission_acl_view, permission_acl_edit, permission_acl_view,
permission_document_delete, permission_document_download, permission_document_delete, permission_document_download,
permission_document_edit, permission_document_new_version, permission_document_edit, permission_document_new_version,
permission_document_print, permission_document_properties_edit, permission_document_print,
permission_document_restore, permission_document_trash, permission_document_properties_edit,
permission_document_version_revert, permission_document_restore, permission_document_tools,
permission_document_trash, permission_document_version_revert,
permission_document_version_view, permission_document_view, permission_document_version_view, permission_document_view,
permission_events_view, permission_transformation_create, permission_events_view, permission_transformation_create,
permission_transformation_delete, permission_transformation_delete,
permission_transformation_edit, permission_transformation_view, permission_transformation_edit,
permission_transformation_view,
) )
) )
@@ -224,13 +229,13 @@ class DocumentsApp(MayanAppConfig):
model=Document, manager_name='passthrough' model=Document, manager_name='passthrough'
) )
ModelPermission.register_inheritance( ModelPermission.register_inheritance(
model=DocumentPage, related='document_version__document', model=DocumentPage, related='document',
) )
ModelPermission.register_manager( ModelPermission.register_manager(
model=DocumentPage, manager_name='passthrough' model=DocumentPage, manager_name='passthrough'
) )
ModelPermission.register_inheritance( ModelPermission.register_inheritance(
model=DocumentPageResult, related='document_version__document', model=DocumentPageResult, related='document',
) )
ModelPermission.register_manager( ModelPermission.register_manager(
model=DocumentPageResult, manager_name='passthrough' model=DocumentPageResult, manager_name='passthrough'
@@ -241,6 +246,9 @@ class DocumentsApp(MayanAppConfig):
ModelPermission.register_inheritance( ModelPermission.register_inheritance(
model=DocumentVersion, related='document', model=DocumentVersion, related='document',
) )
ModelPermission.register_inheritance(
model=DocumentVersionPage, related='document_version',
)
# Document and document page thumbnail widget # Document and document page thumbnail widget
document_page_thumbnail_widget = DocumentPageThumbnailWidget() document_page_thumbnail_widget = DocumentPageThumbnailWidget()
@@ -454,7 +462,7 @@ class DocumentsApp(MayanAppConfig):
link_document_quick_download, link_document_download, link_document_quick_download, link_document_download,
link_document_clear_transformations, link_document_clear_transformations,
link_document_clone_transformations, link_document_clone_transformations,
link_document_update_page_count, link_document_pages_reset,
), sources=(Document,) ), sources=(Document,)
) )
menu_object.bind_links( menu_object.bind_links(
@@ -495,7 +503,7 @@ class DocumentsApp(MayanAppConfig):
link_document_multiple_favorites_remove, link_document_multiple_favorites_remove,
link_document_multiple_clear_transformations, link_document_multiple_clear_transformations,
link_document_multiple_trash, link_document_multiple_download, link_document_multiple_trash, link_document_multiple_download,
link_document_multiple_update_page_count, link_document_multiple_pages_reset,
link_document_multiple_document_type_edit, link_document_multiple_document_type_edit,
), sources=(Document,) ), sources=(Document,)
) )
@@ -547,6 +555,17 @@ class DocumentsApp(MayanAppConfig):
link_document_version_return_list link_document_version_return_list
), sources=(DocumentVersion,) ), sources=(DocumentVersion,)
) )
menu_multi_item.bind_links(
links=(
link_document_version_multiple_page_count_update,
), sources=(DocumentVersion,)
)
menu_object.bind_links(
links=(
link_document_version_page_count_update,
), sources=(DocumentVersion,)
)
menu_list_facet.bind_links( menu_list_facet.bind_links(
links=(link_document_version_view,), sources=(DocumentVersion,) links=(link_document_version_view,), sources=(DocumentVersion,)
) )

View File

@@ -32,12 +32,12 @@ class DashboardWidgetDocumentPagesTotal(DashboardWidgetNumeric):
AccessControlList = apps.get_model( AccessControlList = apps.get_model(
app_label='acls', model_name='AccessControlList' app_label='acls', model_name='AccessControlList'
) )
DocumentPage = apps.get_model( DocumentVersionPage = apps.get_model(
app_label='documents', model_name='DocumentPage' app_label='documents', model_name='DocumentVersionPage'
) )
self.count = AccessControlList.objects.restrict_queryset( self.count = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=request.user, permission=permission_document_view, user=request.user,
queryset=DocumentPage.objects.all() queryset=DocumentVersionPage.objects.all()
).count() ).count()
return super(DashboardWidgetDocumentPagesTotal, self).render(request) return super(DashboardWidgetDocumentPagesTotal, self).render(request)

View File

@@ -36,7 +36,10 @@ icon_document_edit = Icon(
) )
icon_document = Icon(driver_name='fontawesome', symbol='book') icon_document = Icon(driver_name='fontawesome', symbol='book')
icon_document_list = icon_document icon_document_list = icon_document
icon_document_page_count_update = Icon( icon_document_pages_reset = Icon(
driver_name='fontawesome', symbol='copy'
)
icon_document_version_page_count_update = Icon(
driver_name='fontawesome', symbol='copy' driver_name='fontawesome', symbol='copy'
) )
icon_document_preview = Icon(driver_name='fontawesome', symbol='eye') icon_document_preview = Icon(driver_name='fontawesome', symbol='eye')

View File

@@ -168,12 +168,12 @@ link_document_quick_download = Link(
permissions=(permission_document_download,), text=_('Quick download'), permissions=(permission_document_download,), text=_('Quick download'),
view='documents:document_download', view='documents:document_download',
) )
link_document_update_page_count = Link( link_document_pages_reset = Link(
args='resolved_object.pk', args='resolved_object.pk',
icon_class_path='mayan.apps.documents.icons.icon_document_page_count_update', icon_class_path='mayan.apps.documents.icons.icon_document_pages_reset',
permissions=(permission_document_tools,), permissions=(permission_document_tools,),
text=_('Recalculate page count'), text=_('Reset pages'),
view='documents:document_update_page_count' view='documents:document_pages_reset'
) )
link_document_restore = Link( link_document_restore = Link(
permissions=(permission_document_restore,), permissions=(permission_document_restore,),
@@ -217,10 +217,10 @@ link_document_multiple_download = Link(
text=_('Advanced download'), text=_('Advanced download'),
view='documents:document_multiple_download_form' view='documents:document_multiple_download_form'
) )
link_document_multiple_update_page_count = Link( link_document_multiple_pages_reset = Link(
icon_class_path='mayan.apps.documents.icons.icon_document_page_count_update', icon_class_path='mayan.apps.documents.icons.icon_document_pages_reset',
text=_('Recalculate page count'), text=_('Reset pages'),
view='documents:document_multiple_update_page_count' view='documents:document_multiple_pages_reset'
) )
link_document_multiple_restore = Link( link_document_multiple_restore = Link(
icon_class_path='mayan.apps.documents.icons.icon_trashed_document_restore', icon_class_path='mayan.apps.documents.icons.icon_trashed_document_restore',
@@ -246,6 +246,18 @@ link_document_version_return_list = Link(
permissions=(permission_document_version_view,), text=_('Versions'), permissions=(permission_document_version_view,), text=_('Versions'),
view='documents:document_version_list', view='documents:document_version_list',
) )
link_document_version_page_count_update = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.documents.icons.icon_document_version_page_count_update',
permissions=(permission_document_tools,),
text=_('Update page count'),
view='documents:document_version_page_count_update'
)
link_document_version_multiple_page_count_update = Link(
icon_class_path='mayan.apps.documents.icons.icon_document_version_page_count_update',
text=_('Update page count'),
view='documents:document_version_multiple_page_count_update'
)
link_document_version_view = Link( link_document_version_view = Link(
args='resolved_object.pk', args='resolved_object.pk',
icon_class_path='mayan.apps.documents.icons.icon_document_version_view', icon_class_path='mayan.apps.documents.icons.icon_document_version_view',

View File

@@ -35,6 +35,7 @@ DOCUMENT_IMAGES_CACHE_NAME = 'document_images'
DOCUMENT_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.documents.storages.storage_documentimagecache' DOCUMENT_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.documents.storages.storage_documentimagecache'
STUB_EXPIRATION_INTERVAL = 60 * 60 * 24 # 24 hours STUB_EXPIRATION_INTERVAL = 60 * 60 * 24 # 24 hours
UPDATE_PAGE_COUNT_RETRY_DELAY = 10 UPDATE_PAGE_COUNT_RETRY_DELAY = 10
UPLOAD_NEW_DOCUMENT_RETRY_DELAY = 10
UPLOAD_NEW_VERSION_RETRY_DELAY = 10 UPLOAD_NEW_VERSION_RETRY_DELAY = 10
PAGE_RANGE_ALL = 'all' PAGE_RANGE_ALL = 'all'
@@ -42,3 +43,5 @@ PAGE_RANGE_RANGE = 'range'
PAGE_RANGE_CHOICES = ( PAGE_RANGE_CHOICES = (
(PAGE_RANGE_ALL, _('All pages')), (PAGE_RANGE_RANGE, _('Page range')) (PAGE_RANGE_ALL, _('All pages')), (PAGE_RANGE_RANGE, _('Page range'))
) )
RETRY_DELAY_DOCUMENT_RESET_PAGES = 30

View File

@@ -28,15 +28,15 @@ class DocumentManager(models.Manager):
class DocumentPageManager(models.Manager): class DocumentPageManager(models.Manager):
def get_by_natural_key(self, page_number, document_version_natural_key): def get_by_natural_key(self, page_number, document_version_natural_key):
DocumentVersion = apps.get_model( Document = apps.get_model(
app_label='documents', model_name='DocumentVersion' app_label='documents', model_name='Document'
) )
try: try:
document_version = DocumentVersion.objects.get_by_natural_key(*document_version_natural_key) document = Document.objects.get_by_natural_key(*document_version_natural_key)
except DocumentVersion.DoesNotExist: except Document.DoesNotExist:
raise self.model.DoesNotExist raise self.model.DoesNotExist
return self.get(document_version__pk=document_version.pk, page_number=page_number) return self.get(document__pk=document.pk, page_number=page_number)
def get_queryset(self): def get_queryset(self):
return models.QuerySet( return models.QuerySet(
@@ -124,6 +124,19 @@ class DocumentVersionManager(models.Manager):
return self.get(document__pk=document.pk, checksum=checksum) return self.get(document__pk=document.pk, checksum=checksum)
class DocumentVersionPageManager(models.Manager):
def get_by_natural_key(self, page_number, document_version_natural_key):
DocumentVersion = apps.get_model(
app_label='documents', model_name='DocumentVersion'
)
try:
document_version = DocumentVersion.objects.get_by_natural_key(*document_version_natural_key)
except DocumentVersion.DoesNotExist:
raise self.model.DoesNotExist
return self.get(document_version__pk=document_version.pk, page_number=page_number)
class DuplicatedDocumentManager(models.Manager): class DuplicatedDocumentManager(models.Manager):
def clean_empty_duplicate_lists(self): def clean_empty_duplicate_lists(self):
self.filter(documents=None).delete() self.filter(documents=None).delete()

View File

@@ -0,0 +1,38 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('documents', '0051_documentpage_enabled'),
]
operations = [
migrations.DeleteModel(
name='DocumentPageResult',
),
migrations.RenameModel('DocumentPage', 'DocumentVersionPage'),
migrations.AlterField(
model_name='documentversionpage',
name='document_version',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='pages', to='documents.DocumentVersion',
verbose_name='Document version'
),
),
migrations.RemoveField(
model_name='documentversionpage',
name='enabled',
),
migrations.AlterModelOptions(
name='documentversionpage',
options={
'ordering': ('page_number',),
'verbose_name': 'Document version page',
'verbose_name_plural': 'Document version pages'
},
),
]

View File

@@ -0,0 +1,57 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('documents', '0052_rename_document_page'),
]
operations = [
migrations.CreateModel(
name='DocumentPage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('enabled', models.BooleanField(default=True, verbose_name='Enabled')),
('page_number', models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='Page number')),
('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pages', to='documents.Document', verbose_name='Document')),
],
options={
'unique_together': set([('document', 'page_number')]),
'verbose_name': 'Document page',
'verbose_name_plural': 'Document pages',
'ordering': ('page_number',),
},
),
migrations.CreateModel(
name='DocumentPageResult',
fields=[
],
options={
'verbose_name': 'Document page result',
'verbose_name_plural': 'Document pages result',
'ordering': ('document', 'page_number'),
'proxy': True,
'indexes': [],
},
bases=('documents.documentpage',),
),
migrations.CreateModel(
name='DocumentVersionPageResult',
fields=[
],
options={
'verbose_name': 'Document version page',
'verbose_name_plural': 'Document version pages',
'ordering': ('document_version__document', 'page_number'),
'proxy': True,
'indexes': [],
},
bases=('documents.documentversionpage',),
),
]

View File

@@ -0,0 +1,56 @@
from __future__ import unicode_literals
from django.db import migrations
def get_latest_version(document):
return document.versions.order_by('timestamp').last()
def operation_reset_document_pages(apps, schema_editor):
Document = apps.get_model(app_label='documents', model_name='Document')
# Define inside the function to use the migration's apps instance
def pages_reset(document):
ContentType = apps.get_model('contenttypes', 'ContentType')
DocumentVersionPage = apps.get_model(
app_label='documents', model_name='DocumentVersionPage'
)
content_type = ContentType.objects.get_for_model(
model=DocumentVersionPage
)
for document_page in document.pages.all():
document_page.delete()
for version_page in get_latest_version(document=document).pages.all():
document_page = document.pages.create(
content_type=content_type,
page_number=version_page.page_number,
object_id=version_page.pk,
)
for document in Document.objects.using(schema_editor.connection.alias).all():
pages_reset(document=document)
def operation_reset_document_pages_reverse(apps, schema_editor):
Document = apps.get_model(app_label='documents', model_name='Document')
for document in Document.objects.using(schema_editor.connection.alias).all():
for document_page in document.pages.all():
document_page.delete()
class Migration(migrations.Migration):
dependencies = [
('documents', '0053_create_document_page_and_result_models'),
]
operations = [
migrations.RunPython(
code=operation_reset_document_pages,
reverse_code=operation_reset_document_pages_reverse
),
]

View File

@@ -2,4 +2,5 @@ from .document_models import * # NOQA
from .document_page_models import * # NOQA from .document_page_models import * # NOQA
from .document_type_models import * # NOQA from .document_type_models import * # NOQA
from .document_version_models import * # NOQA from .document_version_models import * # NOQA
from .document_version_page_models import * # NOQA
from .misc_models import * # NOQA from .misc_models import * # NOQA

View File

@@ -5,9 +5,10 @@ import uuid
from django.apps import apps from django.apps import apps
from django.core.files import File from django.core.files import File
from django.db import models from django.db import models, transaction
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
@@ -15,6 +16,7 @@ from ..events import (
event_document_create, event_document_properties_edit, event_document_create, event_document_properties_edit,
event_document_type_change, event_document_type_change,
) )
from ..literals import DOCUMENT_IMAGES_CACHE_NAME
from ..managers import DocumentManager, PassthroughManager, TrashCanManager from ..managers import DocumentManager, PassthroughManager, TrashCanManager
from ..settings import setting_language from ..settings import setting_language
from ..signals import post_document_type_change from ..signals import post_document_type_change
@@ -102,6 +104,26 @@ class Document(models.Model):
) )
return RecentDocument.objects.add_document_for_user(user, self) return RecentDocument.objects.add_document_for_user(user, self)
@cached_property
def cache(self):
Cache = apps.get_model(app_label='file_caching', model_name='Cache')
return Cache.objects.get(name=DOCUMENT_IMAGES_CACHE_NAME)
@cached_property
def cache_partition(self):
partition, created = self.cache.partitions.get_or_create(
name='document-{}'.format(self.uuid)
)
return partition
@property
def checksum(self):
return self.latest_version.checksum
@property
def date_updated(self):
return self.latest_version.timestamp
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
to_trash = kwargs.pop('to_trash', True) to_trash = kwargs.pop('to_trash', True)
@@ -126,25 +148,37 @@ class Document(models.Model):
else: else:
return False return False
@property
def file_mime_encoding(self):
return self.latest_version.encoding
@property
def file_mimetype(self):
return self.latest_version.mimetype
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
viewname='documents:document_preview', kwargs={'pk': self.pk} viewname='documents:document_preview', kwargs={'pk': self.pk}
) )
def get_api_image_url(self, *args, **kwargs): def get_api_image_url(self, *args, **kwargs):
latest_version = self.latest_version first_page = self.pages.first()
if latest_version: if first_page:
return latest_version.get_api_image_url(*args, **kwargs) return first_page.get_api_image_url(*args, **kwargs)
@property @property
def is_in_trash(self): def is_in_trash(self):
return self.in_trash return self.in_trash
@property
def latest_version(self):
return self.versions.order_by('timestamp').last()
def natural_key(self): def natural_key(self):
return (self.uuid,) return (self.uuid,)
natural_key.dependencies = ['documents.DocumentType'] natural_key.dependencies = ['documents.DocumentType']
def new_version(self, file_object, comment=None, _user=None): def new_version(self, file_object, append_pages=False, comment=None, _user=None):
logger.info('Creating new document version for document: %s', self) logger.info('Creating new document version for document: %s', self)
DocumentVersion = apps.get_model( DocumentVersion = apps.get_model(
app_label='documents', model_name='DocumentVersion' app_label='documents', model_name='DocumentVersion'
@@ -153,9 +187,10 @@ class Document(models.Model):
document_version = DocumentVersion( document_version = DocumentVersion(
document=self, comment=comment or '', file=File(file_object) document=self, comment=comment or '', file=File(file_object)
) )
document_version.save(_user=_user) document_version.save(append_pages=append_pages, _user=_user)
logger.info('New document version queued for document: %s', self) logger.info('New document version queued for document: %s', self)
return document_version return document_version
def open(self, *args, **kwargs): def open(self, *args, **kwargs):
@@ -165,6 +200,34 @@ class Document(models.Model):
""" """
return self.latest_version.open(*args, **kwargs) return self.latest_version.open(*args, **kwargs)
@property
def page_count(self):
return self.pages.count()
@property
def pages(self):
return self.pages.all()
@property
def pages_all(self):
DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage'
)
return DocumentPage.passthrough.filter(document=self)
def pages_reset(self, update_page_count=True):
with transaction.atomic():
for page in self.pages.all():
page.delete()
if update_page_count:
self.latest_version.update_page_count()
for version_page in self.latest_version.pages.all():
self.pages.create(
content_object=version_page
)
def restore(self): def restore(self):
self.in_trash = False self.in_trash = False
self.save() self.save()
@@ -209,53 +272,3 @@ class Document(models.Model):
@property @property
def size(self): def size(self):
return self.latest_version.size return self.latest_version.size
# Compatibility methods
@property
def checksum(self):
return self.latest_version.checksum
@property
def date_updated(self):
return self.latest_version.timestamp
@property
def file_mime_encoding(self):
return self.latest_version.encoding
@property
def file_mimetype(self):
return self.latest_version.mimetype
@property
def latest_version(self):
return self.versions.order_by('timestamp').last()
@property
def page_count(self):
return self.latest_version.page_count
@property
def pages_all(self):
try:
return self.latest_version.pages_all
except AttributeError:
# Document has no version yet
DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage'
)
return DocumentPage.objects.none()
@property
def pages(self):
try:
return self.latest_version.pages
except AttributeError:
# Document has no version yet
DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage'
)
return DocumentPage.objects.none()

View File

@@ -4,14 +4,16 @@ import logging
from furl import furl from furl import furl
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models import Max
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION from mayan.apps.converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION
from mayan.apps.converter.models import LayerTransformation from mayan.apps.converter.models import LayerTransformation
from mayan.apps.converter.transformations import ( from mayan.apps.converter.transformations import (
BaseTransformation, TransformationResize, TransformationRotate, BaseTransformation, TransformationResize, TransformationRotate,
@@ -21,12 +23,11 @@ from mayan.apps.converter.utils import get_converter_class
from ..managers import DocumentPageManager from ..managers import DocumentPageManager
from ..settings import ( from ..settings import (
setting_disable_base_image_cache, setting_disable_transformed_image_cache,
setting_display_width, setting_display_height, setting_zoom_max_level, setting_display_width, setting_display_height, setting_zoom_max_level,
setting_zoom_min_level setting_zoom_min_level
) )
from .document_version_models import DocumentVersion from .document_models import Document
__all__ = ('DocumentPage', 'DocumentPageResult') __all__ = ('DocumentPage', 'DocumentPageResult')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -35,16 +36,22 @@ logger = logging.getLogger(__name__)
@python_2_unicode_compatible @python_2_unicode_compatible
class DocumentPage(models.Model): class DocumentPage(models.Model):
""" """
Model that describes a document version page Model that describes a document page
""" """
document_version = models.ForeignKey( document = models.ForeignKey(
on_delete=models.CASCADE, related_name='version_pages', to=DocumentVersion, on_delete=models.CASCADE, related_name='pages', to=Document,
verbose_name=_('Document version') verbose_name=_('Document')
) )
enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
page_number = models.PositiveIntegerField( page_number = models.PositiveIntegerField(
db_index=True, default=1, editable=False, db_index=True, blank=True, null=True, verbose_name=_('Page number')
verbose_name=_('Page number') )
content_type = models.ForeignKey(
on_delete=models.CASCADE, to=ContentType
)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey(
ct_field='content_type', fk_field='object_id'
) )
objects = DocumentPageManager() objects = DocumentPageManager()
@@ -52,6 +59,7 @@ class DocumentPage(models.Model):
class Meta: class Meta:
ordering = ('page_number',) ordering = ('page_number',)
unique_together = ('document', 'page_number')
verbose_name = _('Document page') verbose_name = _('Document page')
verbose_name_plural = _('Document pages') verbose_name_plural = _('Document pages')
@@ -60,7 +68,7 @@ class DocumentPage(models.Model):
@cached_property @cached_property
def cache_partition(self): def cache_partition(self):
partition, created = self.document_version.cache.partitions.get_or_create( partition, created = self.document.cache.partitions.get_or_create(
name=self.uuid name=self.uuid
) )
return partition return partition
@@ -69,19 +77,15 @@ class DocumentPage(models.Model):
self.cache_partition.delete() self.cache_partition.delete()
super(DocumentPage, self).delete(*args, **kwargs) super(DocumentPage, self).delete(*args, **kwargs)
def detect_orientation(self): #def detect_orientation(self):
with self.document_version.open() as file_object: # with self.document_version.open() as file_object:
converter = get_converter_class()( # converter = get_converter_class()(
file_object=file_object, # file_object=file_object,
mime_type=self.document_version.mimetype # mime_type=self.document_version.mimetype
) # )
return converter.detect_orientation( # return converter.detect_orientation(
page_number=self.page_number # page_number=self.page_number
) # )
@property
def document(self):
return self.document_version.document
def generate_image(self, user=None, **kwargs): def generate_image(self, user=None, **kwargs):
transformation_list = self.get_combined_transformation_list(user=user, **kwargs) transformation_list = self.get_combined_transformation_list(user=user, **kwargs)
@@ -90,7 +94,7 @@ class DocumentPage(models.Model):
# Check is transformed image is available # Check is transformed image is available
logger.debug('transformations cache filename: %s', combined_cache_filename) logger.debug('transformations cache filename: %s', combined_cache_filename)
if not setting_disable_transformed_image_cache.value and self.cache_partition.get_file(filename=combined_cache_filename): if self.cache_partition.get_file(filename=combined_cache_filename):
logger.debug( logger.debug(
'transformations cache file "%s" found', combined_cache_filename 'transformations cache file "%s" found', combined_cache_filename
) )
@@ -128,8 +132,7 @@ class DocumentPage(models.Model):
final_url.args = kwargs final_url.args = kwargs
final_url.path = reverse( final_url.path = reverse(
viewname='rest_api:documentpage-image', kwargs={ viewname='rest_api:documentpage-image', kwargs={
'pk': self.document.pk, 'version_pk': self.document_version.pk, 'pk': self.document.pk, 'page_pk': self.pk
'page_pk': self.pk
} }
) )
final_url.args['_hash'] = transformations_hash final_url.args['_hash'] = transformations_hash
@@ -190,12 +193,12 @@ class DocumentPage(models.Model):
return transformation_list return transformation_list
def get_image(self, transformations=None): def get_image(self, transformations=None):
cache_filename = 'base_image' cache_filename = 'document_page'
logger.debug('Page cache filename: %s', cache_filename) logger.debug('Page cache filename: %s', cache_filename)
cache_file = self.cache_partition.get_file(filename=cache_filename) cache_file = self.cache_partition.get_file(filename=cache_filename)
if not setting_disable_base_image_cache.value and cache_file: if cache_file:
logger.debug('Page cache file "%s" found', cache_filename) logger.debug('Page cache file "%s" found', cache_filename)
with cache_file.open() as file_object: with cache_file.open() as file_object:
@@ -216,14 +219,25 @@ class DocumentPage(models.Model):
logger.debug('Page cache file "%s" not found', cache_filename) logger.debug('Page cache file "%s" not found', cache_filename)
try: try:
with self.document_version.get_intermediate_file() as file_object: #with self.document_version.get_intermediate_file() as file_object:
#Render or get cached document version page
#self.content_object.generate_image()
self.content_object.get_image()
cache_filename = 'base_image'
cache_file = self.content_object.cache_partition.get_file(filename=cache_filename)
with cache_file.open() as file_object:
converter = get_converter_class()( converter = get_converter_class()(
file_object=file_object file_object=file_object
) )
converter.seek_page(page_number=self.page_number - 1) converter.seek_page(page_number=0)
#self.page_number - 1)
page_image = converter.get_page() page_image = converter.get_page()
cache_filename = 'document_page'
# Since open "wb+" doesn't create files, create it explicitly # Since open "wb+" doesn't create files, create it explicitly
with self.cache_partition.create_file(filename=cache_filename) as file_object: with self.cache_partition.create_file(filename=cache_filename) as file_object:
file_object.write(page_image.getvalue()) file_object.write(page_image.getvalue())
@@ -241,28 +255,39 @@ class DocumentPage(models.Model):
) )
raise raise
def get_label(self):
return _(
'Page %(page_number)d out of %(total_pages)d of %(document)s'
) % {
'document': force_text(self.document),
'page_number': self.page_number,
'total_pages': self.document.pages_all.count()
}
get_label.short_description = _('Label')
@property @property
def is_in_trash(self): def is_in_trash(self):
return self.document.is_in_trash return self.document.is_in_trash
def get_label(self):
return _(
'Page %(page_num)d out of %(total_pages)d of %(document)s'
) % {
'document': force_text(self.document),
'page_num': self.page_number,
'total_pages': self.document_version.pages_all.count()
}
get_label.short_description = _('Label')
def natural_key(self): def natural_key(self):
return (self.page_number, self.document_version.natural_key()) return (self.page_number, self.document.natural_key())
natural_key.dependencies = ['documents.DocumentVersion'] natural_key.dependencies = ['documents.Document']
def save(self, *args, **kwargs):
if not self.page_number:
last_page_number = DocumentPage.objects.filter(
document=self.document
).aggregate(Max('page_number'))['page_number__max']
if last_page_number is not None:
self.page_number = last_page_number + 1
else:
self.page_number = 1
super(DocumentPage, self).save(*args, **kwargs)
@property @property
def siblings(self): def siblings(self):
return DocumentPage.objects.filter( return DocumentPage.objects.filter(
document_version=self.document_version document=self.document
) )
@property @property
@@ -271,12 +296,12 @@ class DocumentPage(models.Model):
Make cache UUID a mix of version ID and page ID to avoid using stale Make cache UUID a mix of version ID and page ID to avoid using stale
images images
""" """
return '{}-{}'.format(self.document_version.uuid, self.pk) return '{}-{}'.format(self.document.uuid, self.pk)
class DocumentPageResult(DocumentPage): class DocumentPageResult(DocumentPage):
class Meta: class Meta:
ordering = ('document_version__document', 'page_number') ordering = ('document', 'page_number')
proxy = True proxy = True
verbose_name = _('Document page') verbose_name = _('Document page result')
verbose_name_plural = _('Document pages') verbose_name_plural = _('Document pages result')

View File

@@ -15,15 +15,13 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.converter.exceptions import InvalidOfficeFormat, PageCountError from mayan.apps.converter.exceptions import InvalidOfficeFormat, PageCountError
from mayan.apps.converter.layers import layer_saved_transformations
from mayan.apps.converter.transformations import TransformationRotate
from mayan.apps.converter.utils import get_converter_class from mayan.apps.converter.utils import get_converter_class
from mayan.apps.mimetype.api import get_mimetype from mayan.apps.mimetype.api import get_mimetype
from ..events import event_document_new_version, event_document_version_revert from ..events import event_document_new_version, event_document_version_revert
from ..literals import DOCUMENT_IMAGES_CACHE_NAME from ..literals import DOCUMENT_IMAGES_CACHE_NAME
from ..managers import DocumentVersionManager from ..managers import DocumentVersionManager
from ..settings import setting_fix_orientation, setting_hash_block_size from ..settings import setting_hash_block_size
from ..signals import post_document_created, post_version_upload from ..signals import post_document_created, post_version_upload
from ..storages import storage_documentversion from ..storages import storage_documentversion
@@ -152,15 +150,6 @@ class DocumentVersion(models.Model):
""" """
return self.file.storage.exists(self.file.name) return self.file.storage.exists(self.file.name)
def fix_orientation(self):
for page in self.pages.all():
degrees = page.detect_orientation()
if degrees:
layer_saved_transformations.add_to_object(
obj=page, transformation=TransformationRotate,
arguments='{{"degrees": {}}}'.format(360 - degrees)
)
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
viewname='documents:document_version_view', kwargs={ viewname='documents:document_version_view', kwargs={
@@ -246,23 +235,12 @@ class DocumentVersion(models.Model):
return result return result
@property #@property
def pages_all(self): #def page_count(self):
DocumentPage = apps.get_model( # """
app_label='documents', model_name='DocumentPage' # The number of pages that the document posses.
) # """
return DocumentPage.passthrough.filter(document_version=self) # return self.pages.count()
@property
def pages(self):
return self.version_pages.all()
@property
def page_count(self):
"""
The number of pages that the document posses.
"""
return self.pages.count()
def revert(self, _user=None): def revert(self, _user=None):
""" """
@@ -285,6 +263,7 @@ class DocumentVersion(models.Model):
Overloaded save method that updates the document version's checksum, Overloaded save method that updates the document version's checksum,
mimetype, and page count when created mimetype, and page count when created
""" """
append_pages = kwargs.pop('append_pages', False)
user = kwargs.pop('_user', None) user = kwargs.pop('_user', None)
new_document_version = not self.pk new_document_version = not self.pk
@@ -304,10 +283,8 @@ class DocumentVersion(models.Model):
# Only do this for new documents # Only do this for new documents
self.update_checksum(save=False) self.update_checksum(save=False)
self.update_mimetype(save=False) self.update_mimetype(save=False)
self.save() self.save(append_pages=append_pages, _user=user)
self.update_page_count(save=False) self.update_page_count(save=False)
if setting_fix_orientation.value:
self.fix_orientation()
logger.info( logger.info(
'New document version "%s" created for document: %s', 'New document version "%s" created for document: %s',
@@ -337,6 +314,14 @@ class DocumentVersion(models.Model):
sender=Document, instance=self.document sender=Document, instance=self.document
) )
if append_pages:
for version_page in self.pages.all():
self.document.pages.create(
content_object=version_page
)
else:
self.document.pages_reset(update_page_count=False)
def save_to_file(self, file_object): def save_to_file(self, file_object):
""" """
Save a copy of the document from the document storage backend Save a copy of the document from the document storage backend
@@ -410,7 +395,7 @@ class DocumentVersion(models.Model):
pass pass
else: else:
DocumentPage = apps.get_model( DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage' app_label='documents', model_name='DocumentVersionPage'
) )
with transaction.atomic(): with transaction.atomic():

View File

@@ -0,0 +1,271 @@
from __future__ import absolute_import, unicode_literals
import logging
from furl import furl
from django.db import models
from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from mayan.apps.converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION
from mayan.apps.converter.models import LayerTransformation
from mayan.apps.converter.transformations import (
BaseTransformation, TransformationResize, TransformationRotate,
TransformationZoom
)
from mayan.apps.converter.utils import get_converter_class
from ..managers import DocumentVersionPageManager
from ..settings import (
setting_display_width, setting_display_height, setting_zoom_max_level,
setting_zoom_min_level
)
from .document_version_models import DocumentVersion
__all__ = ('DocumentVersionPage', 'DocumentVersionPageResult')
logger = logging.getLogger(__name__)
@python_2_unicode_compatible
class DocumentVersionPage(models.Model):
"""
Model that describes a document version page
"""
document_version = models.ForeignKey(
on_delete=models.CASCADE, related_name='pages', to=DocumentVersion,
verbose_name=_('Document version')
)
page_number = models.PositiveIntegerField(
db_index=True, default=1, editable=False,
verbose_name=_('Page number')
)
objects = DocumentVersionPageManager()
class Meta:
ordering = ('page_number',)
verbose_name = _('Document version page')
verbose_name_plural = _('Document version pages')
def __str__(self):
return self.get_label()
@cached_property
def cache_partition(self):
partition, created = self.document_version.cache.partitions.get_or_create(
name=self.uuid
)
return partition
def delete(self, *args, **kwargs):
self.cache_partition.delete()
super(DocumentVersionPage, self).delete(*args, **kwargs)
@property
def document(self):
return self.document_version.document
def generate_image(self, user=None, **kwargs):
transformation_list = self.get_combined_transformation_list(user=user, **kwargs)
combined_cache_filename = BaseTransformation.combine(transformation_list)
# Check is transformed image is available
logger.debug('transformations cache filename: %s', combined_cache_filename)
if self.cache_partition.get_file(filename=combined_cache_filename):
logger.debug(
'transformations cache file "%s" found', combined_cache_filename
)
else:
logger.debug(
'transformations cache file "%s" not found', combined_cache_filename
)
image = self.get_image(transformations=transformation_list)
with self.cache_partition.create_file(filename=combined_cache_filename) as file_object:
file_object.write(image.getvalue())
return combined_cache_filename
def get_absolute_url(self):
return reverse(
viewname='documents:document_version_page_view', kwargs={
'pk': self.pk
}
)
def get_api_image_url(self, *args, **kwargs):
"""
Create an unique URL combining:
- the page's image URL
- the interactive argument
- a hash from the server side and interactive transformations
The purpose of this unique URL is to allow client side caching
if document page images.
"""
transformations_hash = BaseTransformation.combine(
self.get_combined_transformation_list(*args, **kwargs)
)
kwargs.pop('transformations', None)
final_url = furl()
final_url.args = kwargs
final_url.path = reverse(
viewname='rest_api:documentversionpage-image', kwargs={
'pk': self.document.pk, 'version_pk': self.document_version.pk,
'page_pk': self.pk
}
)
final_url.args['_hash'] = transformations_hash
return final_url.tostr()
def get_combined_transformation_list(self, user=None, *args, **kwargs):
"""
Return a list of transformation containing the server side
document page transformation as well as tranformations created
from the arguments as transient interactive transformation.
"""
# Convert arguments into transformations
transformations = kwargs.get('transformations', [])
# Set sensible defaults if the argument is not specified or if the
# argument is None
width = kwargs.get('width', setting_display_width.value) or setting_display_width.value
height = kwargs.get('height', setting_display_height.value) or setting_display_height.value
rotation = kwargs.get('rotation', DEFAULT_ROTATION) or DEFAULT_ROTATION
zoom_level = kwargs.get('zoom', DEFAULT_ZOOM_LEVEL) or DEFAULT_ZOOM_LEVEL
if zoom_level < setting_zoom_min_level.value:
zoom_level = setting_zoom_min_level.value
if zoom_level > setting_zoom_max_level.value:
zoom_level = setting_zoom_max_level.value
# Generate transformation hash
transformation_list = []
maximum_layer_order = kwargs.get('maximum_layer_order', None)
# Stored transformations first
for stored_transformation in LayerTransformation.objects.get_for_object(
self, maximum_layer_order=maximum_layer_order, as_classes=True,
user=user
):
transformation_list.append(stored_transformation)
# Interactive transformations second
for transformation in transformations:
transformation_list.append(transformation)
if rotation:
transformation_list.append(
TransformationRotate(degrees=rotation)
)
if width:
transformation_list.append(
TransformationResize(width=width, height=height)
)
if zoom_level:
transformation_list.append(TransformationZoom(percent=zoom_level))
return transformation_list
def get_image(self, transformations=None):
cache_filename = 'base_image'
logger.debug('Page cache filename: %s', cache_filename)
cache_file = self.cache_partition.get_file(filename=cache_filename)
if cache_file:
logger.debug('Page cache file "%s" found', cache_filename)
with cache_file.open() as file_object:
converter = get_converter_class()(
file_object=file_object
)
converter.seek_page(page_number=0)
# This code is also repeated below to allow using a context
# manager with cache_file.open and close it automatically.
# Apply runtime transformations
for transformation in transformations or []:
converter.transform(transformation=transformation)
return converter.get_page()
else:
logger.debug('Page cache file "%s" not found', cache_filename)
try:
with self.document_version.get_intermediate_file() as file_object:
converter = get_converter_class()(
file_object=file_object
)
converter.seek_page(page_number=self.page_number - 1)
page_image = converter.get_page()
# Since open "wb+" doesn't create files, create it explicitly
with self.cache_partition.create_file(filename=cache_filename) as file_object:
file_object.write(page_image.getvalue())
# Apply runtime transformations
for transformation in transformations or []:
converter.transform(transformation=transformation)
return converter.get_page()
except Exception as exception:
# Cleanup in case of error
logger.error(
'Error creating page cache file "%s"; %s',
cache_filename, exception
)
raise
def get_label(self):
return _(
'Version page %(page_number)d out of %(total_pages)d of %(document)s'
) % {
'document': force_text(self.document),
'page_number': self.page_number,
'total_pages': self.document_version.pages.count()
}
get_label.short_description = _('Label')
@property
def is_in_trash(self):
return self.document_version.document.is_in_trash
def natural_key(self):
return (self.page_number, self.document_version.natural_key())
natural_key.dependencies = ['documents.DocumentVersion']
@property
def siblings(self):
return DocumentVersionPage.objects.filter(
document_version=self.document_version
)
@property
def uuid(self):
"""
Make cache UUID a mix of version ID and page ID to avoid using stale
images
"""
return '{}-{}'.format(self.document_version.uuid, self.pk)
class DocumentVersionPageResult(DocumentVersionPage):
class Meta:
ordering = ('document_version__document', 'page_number')
proxy = True
verbose_name = _('Document version page')
verbose_name_plural = _('Document version pages')

View File

@@ -30,6 +30,10 @@ queue_converter.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_generate_document_page_image', dotted_path='mayan.apps.documents.tasks.task_generate_document_page_image',
label=_('Generate document page image') label=_('Generate document page image')
) )
queue_converter.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_generate_document_version_page_image',
label=_('Generate document version page image')
)
queue_documents.add_task_type( queue_documents.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_delete_document', dotted_path='mayan.apps.documents.tasks.task_delete_document',
@@ -66,6 +70,10 @@ queue_tools.add_task_type(
label=_('Duplicated document scan') label=_('Duplicated document scan')
) )
queue_uploads.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_document_pages_reset',
label=_('Reset document pages')
)
queue_uploads.add_task_type( queue_uploads.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_update_page_count', dotted_path='mayan.apps.documents.tasks.task_update_page_count',
label=_('Update document page count') label=_('Update document page count')
@@ -78,3 +86,7 @@ queue_uploads.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_for', dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_for',
label=_('Scan document duplicates') label=_('Scan document duplicates')
) )
queue_uploads.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_upload_new_document',
label=_('Upload new document')
)

View File

@@ -17,12 +17,20 @@ def transformation_format_uuid(term_string):
return term_string return term_string
def get_queryset_page_search_queryset(): def get_queryset_document_page_search_queryset():
# Ignore documents in trash can # Ignore documents in trash can
DocumentPage = apps.get_model( DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage' app_label='documents', model_name='DocumentPage'
) )
return DocumentPage.objects.filter(document_version__document__in_trash=False) return DocumentPage.objects.filter(document__in_trash=False)
def get_queryset_document_version_page_search_queryset():
# Ignore documents in trash can
DocumentVersionPage = apps.get_model(
app_label='documents', model_name='DocumentVersionPage'
)
return DocumentVersionPage.objects.filter(document_version__document__in_trash=False)
document_search = SearchModel( document_search = SearchModel(
@@ -30,7 +38,6 @@ document_search = SearchModel(
model_name='Document', permission=permission_document_view, model_name='Document', permission=permission_document_view,
serializer_path='mayan.apps.documents.serializers.DocumentSerializer' serializer_path='mayan.apps.documents.serializers.DocumentSerializer'
) )
document_search.add_model_field( document_search.add_model_field(
field='document_type__label', label=_('Document type') field='document_type__label', label=_('Document type')
) )
@@ -50,24 +57,49 @@ document_search.add_model_field(
document_page_search = SearchModel( document_page_search = SearchModel(
app_label='documents', list_mode=LIST_MODE_CHOICE_ITEM, app_label='documents', list_mode=LIST_MODE_CHOICE_ITEM,
model_name='DocumentPage', permission=permission_document_view, model_name='DocumentPage', permission=permission_document_view,
queryset=get_queryset_page_search_queryset, queryset=get_queryset_document_page_search_queryset,
serializer_path='mayan.apps.documents.serializers.DocumentPageSerializer' serializer_path='mayan.apps.documents.serializers.DocumentPageSerializer'
) )
document_version_page_search = SearchModel(
app_label='documents', list_mode=LIST_MODE_CHOICE_ITEM,
model_name='DocumentVersionPage', permission=permission_document_view,
queryset=get_queryset_document_version_page_search_queryset,
serializer_path='mayan.apps.documents.serializers.DocumentVersionPageSerializer'
)
document_page_search.add_model_field( document_page_search.add_model_field(
field='document_version__document__document_type__label', field='document__document_type__label',
label=_('Document type') label=_('Document type')
) )
document_page_search.add_model_field( document_page_search.add_model_field(
field='document_version__document__versions__mimetype', field='document__versions__mimetype',
label=_('MIME type') label=_('MIME type')
) )
document_page_search.add_model_field( document_page_search.add_model_field(
field='document__label', label=_('Label')
)
document_page_search.add_model_field(
field='document__description', label=_('Description')
)
document_page_search.add_model_field(
field='document__versions__checksum', label=_('Checksum')
)
document_version_page_search.add_model_field(
field='document_version__document__document_type__label',
label=_('Document type')
)
document_version_page_search.add_model_field(
field='document_version__document__versions__mimetype',
label=_('MIME type')
)
document_version_page_search.add_model_field(
field='document_version__document__label', label=_('Label') field='document_version__document__label', label=_('Label')
) )
document_page_search.add_model_field( document_version_page_search.add_model_field(
field='document_version__document__description', label=_('Description') field='document_version__document__description', label=_('Description')
) )
document_page_search.add_model_field( document_version_page_search.add_model_field(
field='document_version__checksum', label=_('Checksum') field='document_version__checksum', label=_('Checksum')
) )

View File

@@ -8,42 +8,40 @@ from rest_framework.reverse import reverse
from mayan.apps.common.models import SharedUploadedFile from mayan.apps.common.models import SharedUploadedFile
from .models import ( from .models import (
Document, DocumentVersion, DocumentPage, DocumentType, Document, DocumentPage, DocumentType, DocumentTypeFilename,
DocumentTypeFilename, RecentDocument DocumentVersion, DocumentVersionPage, RecentDocument
) )
from .settings import setting_language from .settings import setting_language
from .tasks import task_upload_new_version from .tasks import task_upload_new_version
class DocumentPageSerializer(serializers.HyperlinkedModelSerializer): class DocumentPageSerializer(serializers.HyperlinkedModelSerializer):
document_version_url = serializers.SerializerMethodField() document_url = serializers.SerializerMethodField()
image_url = serializers.SerializerMethodField() image_url = serializers.SerializerMethodField()
url = serializers.SerializerMethodField() url = serializers.SerializerMethodField()
class Meta: class Meta:
fields = ('document_version_url', 'image_url', 'page_number', 'url') fields = ('document_url', 'image_url', 'page_number', 'url')
model = DocumentPage model = DocumentPage
def get_document_version_url(self, instance): def get_document_url(self, instance):
return reverse( return reverse(
viewname='rest_api:documentversion-detail', args=( viewname='rest_api:document-detail', args=(
instance.document.pk, instance.document_version.pk, instance.document.pk,
), request=self.context['request'], format=self.context['format'] ), request=self.context['request'], format=self.context['format']
) )
def get_image_url(self, instance): def get_image_url(self, instance):
return reverse( return reverse(
viewname='rest_api:documentpage-image', args=( viewname='rest_api:documentpage-image', args=(
instance.document.pk, instance.document_version.pk, instance.document.pk, instance.pk,
instance.pk,
), request=self.context['request'], format=self.context['format'] ), request=self.context['request'], format=self.context['format']
) )
def get_url(self, instance): def get_url(self, instance):
return reverse( return reverse(
viewname='rest_api:documentpage-detail', args=( viewname='rest_api:documentpage-detail', args=(
instance.document.pk, instance.document_version.pk, instance.document.pk, instance.pk,
instance.pk,
), request=self.context['request'], format=self.context['format'] ), request=self.context['request'], format=self.context['format']
) )
@@ -97,6 +95,39 @@ class WritableDocumentTypeSerializer(serializers.ModelSerializer):
return obj.documents.count() return obj.documents.count()
class DocumentVersionPageSerializer(serializers.HyperlinkedModelSerializer):
document_version_url = serializers.SerializerMethodField()
image_url = serializers.SerializerMethodField()
url = serializers.SerializerMethodField()
class Meta:
fields = ('document_version_url', 'image_url', 'page_number', 'url')
model = DocumentVersionPage
def get_document_version_url(self, instance):
return reverse(
viewname='rest_api:documentversion-detail', args=(
instance.document.pk, instance.document_version.pk,
), request=self.context['request'], format=self.context['format']
)
def get_image_url(self, instance):
return reverse(
viewname='rest_api:documentversionpage-image', args=(
instance.document.pk, instance.document_version.pk,
instance.pk,
), request=self.context['request'], format=self.context['format']
)
def get_url(self, instance):
return reverse(
viewname='rest_api:documentversionpage-detail', args=(
instance.document.pk, instance.document_version.pk,
instance.pk,
), request=self.context['request'], format=self.context['format']
)
class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer):
document_url = serializers.SerializerMethodField() document_url = serializers.SerializerMethodField()
download_url = serializers.SerializerMethodField() download_url = serializers.SerializerMethodField()

View File

@@ -38,21 +38,6 @@ setting_documentimagecache_storage_arguments = namespace.add_setting(
'Arguments to pass to the DOCUMENT_CACHE_STORAGE_BACKEND.' 'Arguments to pass to the DOCUMENT_CACHE_STORAGE_BACKEND.'
), ),
) )
setting_disable_base_image_cache = namespace.add_setting(
global_name='DOCUMENTS_DISABLE_BASE_IMAGE_CACHE', default=False,
help_text=_(
'Disables the first cache tier which stores high resolution, '
'non transformed versions of documents\'s pages.'
)
)
setting_disable_transformed_image_cache = namespace.add_setting(
global_name='DOCUMENTS_DISABLE_TRANSFORMED_IMAGE_CACHE', default=False,
help_text=_(
'Disables the second cache tier which stores medium to low '
'resolution, transformed (rotated, zoomed, etc) versions '
'of documents\' pages.'
)
)
setting_display_height = namespace.add_setting( setting_display_height = namespace.add_setting(
global_name='DOCUMENTS_DISPLAY_HEIGHT', default='' global_name='DOCUMENTS_DISPLAY_HEIGHT', default=''
) )
@@ -65,15 +50,6 @@ setting_favorite_count = namespace.add_setting(
'Maximum number of favorite documents to remember per user.' 'Maximum number of favorite documents to remember per user.'
) )
) )
setting_fix_orientation = namespace.add_setting(
global_name='DOCUMENTS_FIX_ORIENTATION', default=False,
help_text=_(
'Detect the orientation of each of the document\'s pages '
'and create a corresponding rotation transformation to '
'display it rightside up. This is an experimental '
'feature and it is disabled by default.'
)
)
setting_hash_block_size = namespace.add_setting( setting_hash_block_size = namespace.add_setting(
global_name='DOCUMENTS_HASH_BLOCK_SIZE', global_name='DOCUMENTS_HASH_BLOCK_SIZE',
default=DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE, help_text=_( default=DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE, help_text=_(

View File

@@ -41,7 +41,7 @@ def new_documents_per_month():
def new_document_pages_per_month(): def new_document_pages_per_month():
DocumentPage = apps.get_model( DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage' app_label='documents', model_name='DocumentVersionPage'
) )
qss = qsstats.QuerySetStats( qss = qsstats.QuerySetStats(
@@ -106,7 +106,7 @@ def new_document_pages_this_month(user=None):
app_label='acls', model_name='AccessControlList' app_label='acls', model_name='AccessControlList'
) )
DocumentPage = apps.get_model( DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage' app_label='documents', model_name='DocumentVersionPage'
) )
queryset = DocumentPage.objects.all() queryset = DocumentPage.objects.all()
@@ -195,7 +195,7 @@ def total_document_version_per_month():
def total_document_page_per_month(): def total_document_page_per_month():
DocumentPage = apps.get_model( DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage' app_label='documents', model_name='DocumentVersionPage'
) )
qss = qsstats.QuerySetStats( qss = qsstats.QuerySetStats(

View File

@@ -9,7 +9,8 @@ from django.db import OperationalError
from mayan.celery import app from mayan.celery import app
from .literals import ( from .literals import (
UPDATE_PAGE_COUNT_RETRY_DELAY, UPLOAD_NEW_VERSION_RETRY_DELAY RETRY_DELAY_DOCUMENT_RESET_PAGES, UPDATE_PAGE_COUNT_RETRY_DELAY,
UPLOAD_NEW_DOCUMENT_RETRY_DELAY, UPLOAD_NEW_VERSION_RETRY_DELAY
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -64,6 +65,25 @@ def task_delete_stubs():
logger.info(msg='Finshed') logger.info(msg='Finshed')
@app.task(bind=True, default_retry_delay=RETRY_DELAY_DOCUMENT_RESET_PAGES, ignore_result=True)
def task_document_pages_reset(self, document_id):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
document = Document.objects.get(pk=document_id)
try:
document.pages_reset()
except OperationalError as exception:
logger.warning(
'Operational error during attempt to reset pages for '
'document: %s; %s. Retrying.', document,
exception
)
raise self.retry(exc=exception)
@app.task() @app.task()
def task_generate_document_page_image(document_page_id, user_id=None, **kwargs): def task_generate_document_page_image(document_page_id, user_id=None, **kwargs):
DocumentPage = apps.get_model( DocumentPage = apps.get_model(
@@ -80,6 +100,22 @@ def task_generate_document_page_image(document_page_id, user_id=None, **kwargs):
return document_page.generate_image(user=user, **kwargs) return document_page.generate_image(user=user, **kwargs)
@app.task()
def task_generate_document_version_page_image(document_version_page_id, user_id=None, **kwargs):
DocumentVersionPage = apps.get_model(
app_label='documents', model_name='DocumentVersionPage'
)
User = get_user_model()
if user_id:
user = User.objects.get(pk=user_id)
else:
user = None
document_version_page = DocumentVersionPage.objects.get(pk=document_version_page_id)
return document_version_page.generate_image(user=user, **kwargs)
@app.task(ignore_result=True) @app.task(ignore_result=True)
def task_scan_duplicates_all(): def task_scan_duplicates_all():
DuplicatedDocument = apps.get_model( DuplicatedDocument = apps.get_model(
@@ -121,8 +157,62 @@ def task_update_page_count(self, version_id):
raise self.retry(exc=exception) raise self.retry(exc=exception)
@app.task(bind=True, default_retry_delay=UPLOAD_NEW_DOCUMENT_RETRY_DELAY, ignore_result=True)
def task_upload_new_document(self, document_type_id, shared_uploaded_file_id):
DocumentType = apps.get_model(
app_label='documents', model_name='DocumentType'
)
SharedUploadedFile = apps.get_model(
app_label='common', model_name='SharedUploadedFile'
)
try:
document_type = DocumentType.objects.get(pk=document_type_id)
shared_file = SharedUploadedFile.objects.get(
pk=shared_uploaded_file_id
)
except OperationalError as exception:
logger.warning(
'Operational error during attempt to retrieve shared data for '
'new document of type: %s; %s. Retrying.', document_type, exception
)
raise self.retry(exc=exception)
try:
with shared_file.open() as file_object:
document_type.new_document(file_object=file_object)
except OperationalError as exception:
logger.warning(
'Operational error during attempt to create new document '
'of type: %s; %s. Retrying.', document_type, exception
)
raise self.retry(exc=exception)
except Exception as exception:
# This except and else block emulate a finally:
logger.error(
'Unexpected error during attempt to create new document '
'of type: %s; %s', document_type, exception
)
try:
shared_file.delete()
except OperationalError as exception:
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)
else:
try:
shared_file.delete()
except OperationalError as exception:
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)
@app.task(bind=True, default_retry_delay=UPLOAD_NEW_VERSION_RETRY_DELAY, ignore_result=True) @app.task(bind=True, default_retry_delay=UPLOAD_NEW_VERSION_RETRY_DELAY, ignore_result=True)
def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, comment=None): def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, append_pages=False, comment=None):
SharedUploadedFile = apps.get_model( SharedUploadedFile = apps.get_model(
app_label='common', model_name='SharedUploadedFile' app_label='common', model_name='SharedUploadedFile'
) )
@@ -157,7 +247,7 @@ def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id,
document=document, comment=comment or '', file=file_object document=document, comment=comment or '', file=file_object
) )
try: try:
document_version.save(_user=user) document_version.save(append_pages=append_pages, _user=user)
except Warning as warning: except Warning as warning:
# New document version are blocked # New document version are blocked
logger.info( logger.info(

View File

@@ -69,6 +69,8 @@ class DocumentTestMixin(object):
self.test_document = document self.test_document = document
self.test_documents.append(document) self.test_documents.append(document)
self.test_document_version = document.latest_version
self.test_document_page = document.pages_all.first()
class DocumentTypeViewTestMixin(object): class DocumentTypeViewTestMixin(object):
@@ -148,6 +150,26 @@ class DocumentVersionTestMixin(object):
) )
class DocumentVersionViewTestMixin(object):
def _request_document_version_list_view(self):
return self.get(
viewname='documents:document_version_list',
kwargs={'pk': self.test_document.pk}
)
def _request_document_version_revert_view(self, document_version):
return self.post(
viewname='documents:document_version_revert',
kwargs={'pk': document_version.pk}
)
def _request_test_document_version_page_count_update_view(self):
return self.post(
viewname='documents:document_version_page_count_update',
kwargs={'pk': self.test_document_version.pk}
)
class DocumentViewTestMixin(object): class DocumentViewTestMixin(object):
def _request_document_properties_view(self): def _request_document_properties_view(self):
return self.get( return self.get(
@@ -200,6 +222,12 @@ class DocumentViewTestMixin(object):
data={'id_list': self.test_document.pk} data={'id_list': self.test_document.pk}
) )
def _request_document_pages_reset_view(self):
return self.post(
viewname='documents:document_pages_reset',
kwargs={'pk': self.test_document.pk}
)
def _request_document_version_download(self, data=None): def _request_document_version_download(self, data=None):
data = data or {} data = data or {}
return self.get( return self.get(
@@ -208,18 +236,6 @@ class DocumentViewTestMixin(object):
}, data=data }, data=data
) )
def _request_document_update_page_count_view(self):
return self.post(
viewname='documents:document_update_page_count',
kwargs={'pk': self.test_document.pk}
)
def _request_document_multiple_update_page_count_view(self):
return self.post(
viewname='documents:document_multiple_update_page_count',
data={'id_list': self.test_document.pk}
)
def _request_document_clear_transformations_view(self): def _request_document_clear_transformations_view(self):
return self.post( return self.post(
viewname='documents:document_clear_transformations', viewname='documents:document_clear_transformations',
@@ -232,8 +248,11 @@ class DocumentViewTestMixin(object):
data={'id_list': self.test_document.pk} data={'id_list': self.test_document.pk}
) )
def _request_empty_trash_view(self): def _request_document_multiple_pages_reset_view(self):
return self.post(viewname='documents:trash_can_empty') return self.post(
viewname='documents:document_multiple_pages_reset',
data={'id_list': self.test_document.pk}
)
def _request_document_print_view(self): def _request_document_print_view(self):
return self.get( return self.get(
@@ -243,3 +262,6 @@ class DocumentViewTestMixin(object):
'page_group': PAGE_RANGE_ALL 'page_group': PAGE_RANGE_ALL
} }
) )
def _request_empty_trash_view(self):
return self.post(viewname='documents:trash_can_empty')

View File

@@ -530,8 +530,7 @@ class DocumentPageAPIViewTestMixin(object):
page = self.test_document.pages.first() page = self.test_document.pages.first()
return self.get( return self.get(
viewname='rest_api:documentpage-image', kwargs={ viewname='rest_api:documentpage-image', kwargs={
'pk': page.document.pk, 'version_pk': page.document_version.pk, 'pk': page.document.pk, 'page_pk': page.pk
'page_pk': page.pk
} }
) )
@@ -552,6 +551,33 @@ class DocumentPageAPIViewTestCase(
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
class DocumentVersionPageAPIViewTestMixin(object):
def _request_document_version_page_image(self):
page = self.test_document_version.pages.first()
return self.get(
viewname='rest_api:documentversionpage-image', kwargs={
'pk': page.document.pk, 'version_pk': page.document_version.pk,
'page_pk': page.pk
}
)
class DocumentVersionPageAPIViewTestCase(
DocumentVersionPageAPIViewTestMixin, DocumentTestMixin, BaseAPITestCase
):
def test_document_version_page_api_image_view_no_access(self):
response = self._request_document_version_page_image()
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_document_version_page_api_image_view_with_access(self):
self.grant_access(
obj=self.test_document, permission=permission_document_view
)
response = self._request_document_version_page_image()
self.assertEqual(response.status_code, status.HTTP_200_OK)
class TrashedDocumentAPIViewTestMixin(object): class TrashedDocumentAPIViewTestMixin(object):
def _request_test_document_api_trash_view(self): def _request_test_document_api_trash_view(self):
return self.delete( return self.delete(
@@ -575,13 +601,10 @@ class TrashedDocumentAPIViewTestMixin(object):
) )
def _request_test_trashed_document_api_image_view(self): def _request_test_trashed_document_api_image_view(self):
latest_version = self.test_document.latest_version
return self.get( return self.get(
viewname='rest_api:documentpage-image', kwargs={ viewname='rest_api:documentpage-image', kwargs={
'pk': latest_version.document.pk, 'pk': self.test_document.pk,
'version_pk': latest_version.pk, 'page_pk': self.test_document.pages.first().pk
'page_pk': latest_version.pages.first().pk
} }
) )

View File

@@ -9,10 +9,10 @@ from ..permissions import (
from .base import GenericDocumentViewTestCase from .base import GenericDocumentViewTestCase
class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase): class DocumentPageDisableViewTestMixin(object):
def setUp(self): def _disable_test_document_page(self):
super(DocumentPageDisableViewTestCase, self).setUp() self.test_document_page.enabled = False
self.test_document_page = self.test_document.pages_all.first() self.test_document_page.save()
def _request_test_document_page_disable_view(self): def _request_test_document_page_disable_view(self):
return self.post( return self.post(
@@ -21,6 +21,31 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase):
} }
) )
def _request_test_document_page_enable_view(self):
return self.post(
viewname='documents:document_page_enable', kwargs={
'pk': self.test_document_page.pk
}
)
def _request_test_document_page_multiple_disable_view(self):
return self.post(
viewname='documents:document_page_multiple_disable', data={
'id_list': self.test_document_page.pk
}
)
def _request_test_document_page_multiple_enable_view(self):
return self.post(
viewname='documents:document_page_multiple_enable', data={
'id_list': self.test_document_page.pk
}
)
class DocumentPageDisableViewTestCase(
DocumentPageDisableViewTestMixin, GenericDocumentViewTestCase
):
def test_document_page_disable_view_no_permission(self): def test_document_page_disable_view_no_permission(self):
test_document_page_count = self.test_document.pages.count() test_document_page_count = self.test_document.pages.count()
@@ -45,13 +70,6 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase):
test_document_page_count, self.test_document.pages.count() test_document_page_count, self.test_document.pages.count()
) )
def _request_test_document_page_multiple_disable_view(self):
return self.post(
viewname='documents:document_page_multiple_disable', data={
'id_list': self.test_document_page.pk
}
)
def test_document_page_multiple_disable_view_no_permission(self): def test_document_page_multiple_disable_view_no_permission(self):
test_document_page_count = self.test_document.pages.count() test_document_page_count = self.test_document.pages.count()
@@ -76,17 +94,6 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase):
test_document_page_count, self.test_document.pages.count() test_document_page_count, self.test_document.pages.count()
) )
def _disable_test_document_page(self):
self.test_document_page.enabled = False
self.test_document_page.save()
def _request_test_document_page_enable_view(self):
return self.post(
viewname='documents:document_page_enable', kwargs={
'pk': self.test_document_page.pk
}
)
def test_document_page_enable_view_no_permission(self): def test_document_page_enable_view_no_permission(self):
self._disable_test_document_page() self._disable_test_document_page()
@@ -114,13 +121,6 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase):
test_document_page_count, self.test_document.pages.count() test_document_page_count, self.test_document.pages.count()
) )
def _request_test_document_page_multiple_enable_view(self):
return self.post(
viewname='documents:document_page_multiple_enable', data={
'id_list': self.test_document_page.pk
}
)
def test_document_page_multiple_enable_view_no_permission(self): def test_document_page_multiple_enable_view_no_permission(self):
self._disable_test_document_page() self._disable_test_document_page()
test_document_page_count = self.test_document.pages.count() test_document_page_count = self.test_document.pages.count()
@@ -148,7 +148,7 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase):
) )
class DocumentPageViewTestCase(GenericDocumentViewTestCase): class DocumentPageViewTestMixin(object):
def _request_test_document_page_list_view(self): def _request_test_document_page_list_view(self):
return self.get( return self.get(
viewname='documents:document_pages', kwargs={ viewname='documents:document_pages', kwargs={
@@ -156,6 +156,18 @@ class DocumentPageViewTestCase(GenericDocumentViewTestCase):
} }
) )
def _request_test_document_page_view(self, document_page):
return self.get(
viewname='documents:document_page_view', kwargs={
'pk': document_page.pk,
}
)
class DocumentPageViewTestCase(
DocumentPageViewTestMixin, GenericDocumentViewTestCase
):
def test_document_page_list_view_no_permission(self): def test_document_page_list_view_no_permission(self):
response = self._request_test_document_page_list_view() response = self._request_test_document_page_list_view()
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@@ -170,13 +182,6 @@ class DocumentPageViewTestCase(GenericDocumentViewTestCase):
response=response, text=self.test_document.label, status_code=200 response=response, text=self.test_document.label, status_code=200
) )
def _request_test_document_page_view(self, document_page):
return self.get(
viewname='documents:document_page_view', kwargs={
'pk': document_page.pk,
}
)
def test_document_page_view_no_permissions(self): def test_document_page_view_no_permissions(self):
response = self._request_test_document_page_view( response = self._request_test_document_page_view(
document_page=self.test_document.pages.first() document_page=self.test_document.pages.first()

View File

@@ -1,21 +1,19 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from ..permissions import ( from ..permissions import (
permission_document_version_revert, permission_document_version_view, permission_document_tools, permission_document_version_revert,
permission_document_version_view,
) )
from .base import GenericDocumentViewTestCase from .base import GenericDocumentViewTestCase
from .literals import TEST_VERSION_COMMENT from .literals import TEST_VERSION_COMMENT
from .mixins import DocumentVersionTestMixin from .mixins import DocumentVersionTestMixin, DocumentVersionViewTestMixin
class DocumentVersionTestCase(DocumentVersionTestMixin, GenericDocumentViewTestCase): class DocumentVersionTestCase(
def _request_document_version_list_view(self): DocumentVersionViewTestMixin, DocumentVersionTestMixin,
return self.get( GenericDocumentViewTestCase
viewname='documents:document_version_list', ):
kwargs={'pk': self.test_document.pk}
)
def test_document_version_list_no_permission(self): def test_document_version_list_no_permission(self):
self._upload_new_version() self._upload_new_version()
@@ -33,12 +31,6 @@ class DocumentVersionTestCase(DocumentVersionTestMixin, GenericDocumentViewTestC
response=response, text=TEST_VERSION_COMMENT, status_code=200 response=response, text=TEST_VERSION_COMMENT, status_code=200
) )
def _request_document_version_revert_view(self, document_version):
return self.post(
viewname='documents:document_version_revert',
kwargs={'pk': document_version.pk}
)
def test_document_version_revert_no_permission(self): def test_document_version_revert_no_permission(self):
first_version = self.test_document.latest_version first_version = self.test_document.latest_version
self._upload_new_version() self._upload_new_version()
@@ -64,3 +56,25 @@ class DocumentVersionTestCase(DocumentVersionTestMixin, GenericDocumentViewTestC
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(self.test_document.versions.count(), 1) self.assertEqual(self.test_document.versions.count(), 1)
def test_document_version_page_count_update_view_no_permission(self):
self.test_document_version.pages.all().delete()
response = self._request_test_document_version_page_count_update_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(self.test_document_version.pages.count(), 0)
def test_document_version_page_count_update_view_with_access(self):
page_count = self.test_document_version.pages.count()
self.test_document_version.pages.all().delete()
self.grant_access(
obj=self.test_document, permission=permission_document_tools
)
response = self._request_test_document_version_page_count_update_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(self.test_document_version.pages.count(), page_count)

View File

@@ -292,46 +292,44 @@ class DocumentsViewsTestCase(
) )
) )
def test_document_update_page_count_view_no_permission(self): def test_document_pages_reset_view_no_permission(self):
self.test_document.pages.all().delete() self.test_document.pages.all().delete()
self.assertEqual(self.test_document.pages.count(), 0)
response = self._request_document_update_page_count_view() response = self._request_document_pages_reset_view()
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertEqual(self.test_document.pages.count(), 0) self.assertEqual(self.test_document.pages.count(), 0)
def test_document_update_page_count_view_with_permission(self): def test_document_pages_reset_view_with_access(self):
# TODO: Revise permission association
page_count = self.test_document.pages.count() page_count = self.test_document.pages.count()
self.test_document.pages.all().delete() self.test_document.pages.all().delete()
self.assertEqual(self.test_document.pages.count(), 0)
self.grant_permission(permission=permission_document_tools) self.grant_access(
obj=self.test_document, permission=permission_document_tools
)
response = self._request_document_update_page_count_view() response = self._request_document_pages_reset_view()
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(self.test_document.pages.count(), page_count) self.assertEqual(self.test_document.pages.count(), page_count)
def test_document_multiple_update_page_count_view_no_permission(self): def test_document_multiple_pages_reset_view_no_permission(self):
self.test_document.pages.all().delete() self.test_document.pages.all().delete()
self.assertEqual(self.test_document.pages.count(), 0)
response = self._request_document_multiple_update_page_count_view() response = self._request_document_multiple_pages_reset_view()
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertEqual(self.test_document.pages.count(), 0) self.assertEqual(self.test_document.pages.count(), 0)
def test_document_multiple_update_page_count_view_with_permission(self): def test_document_multiple_pages_reset_view_with_access(self):
page_count = self.test_document.pages.count() page_count = self.test_document.pages.count()
self.test_document.pages.all().delete() self.test_document.pages.all().delete()
self.assertEqual(self.test_document.pages.count(), 0)
self.grant_permission(permission=permission_document_tools) self.grant_access(
obj=self.test_document, permission=permission_document_tools
)
response = self._request_document_multiple_update_page_count_view() response = self._request_document_multiple_pages_reset_view()
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(self.test_document.pages.count(), page_count) self.assertEqual(self.test_document.pages.count(), page_count)

View File

@@ -1,22 +1,30 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from mayan.apps.common.tests.base import BaseTestCase from mayan.apps.common.tests.base import BaseTestCase
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.search import document_search, document_page_search from ..permissions import permission_document_view
from mayan.apps.documents.tests.mixins import DocumentTestMixin from ..search import document_search, document_page_search
from .mixins import DocumentTestMixin
class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): class DocumentSearchTestMixin(object):
def _perform_document_page_search(self): def _perform_document_page_search(self):
return document_page_search.search( return document_page_search.search(
query_string={'q': self.test_document.label}, user=self._test_case_user query_string={'q': self.test_document.label},
user=self._test_case_user
) )
def _perform_document_search(self): def _perform_document_search(self):
return document_search.search( return document_search.search(
query_string={'q': self.test_document.label}, user=self._test_case_user query_string={'q': self.test_document.label},
user=self._test_case_user
) )
class DocumentSearchTestCase(
DocumentSearchTestMixin, DocumentTestMixin, BaseTestCase
):
def test_document_page_search_no_access(self): def test_document_page_search_no_access(self):
queryset = self._perform_document_page_search() queryset = self._perform_document_page_search()
self.assertFalse(self.test_document.pages.first() in queryset) self.assertFalse(self.test_document.pages.first() in queryset)

View File

@@ -9,7 +9,7 @@ from ..permissions import (
from .base import GenericDocumentViewTestCase from .base import GenericDocumentViewTestCase
class TrashedDocumentTestCase(GenericDocumentViewTestCase): class TrashedDocumentTestMixin(object):
def _request_document_restore_get_view(self): def _request_document_restore_get_view(self):
return self.get( return self.get(
viewname='documents:document_restore', kwargs={ viewname='documents:document_restore', kwargs={
@@ -17,6 +17,48 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
} }
) )
def _request_document_restore_post_view(self):
return self.post(
viewname='documents:document_restore', kwargs={
'pk': self.test_document.pk
}
)
def _request_document_trash_get_view(self):
return self.get(
viewname='documents:document_trash', kwargs={
'pk': self.test_document.pk
}
)
def _request_document_trash_post_view(self):
return self.post(
viewname='documents:document_trash', kwargs={
'pk': self.test_document.pk
}
)
def _request_trashed_document_delete_get_view(self):
return self.get(
viewname='documents:document_delete', kwargs={
'pk': self.test_document.pk
}
)
def _request_trashed_document_delete_post_view(self):
return self.post(
viewname='documents:document_delete', kwargs={
'pk': self.test_document.pk
}
)
def _request_trashed_document_list_view(self):
return self.get(viewname='documents:document_list_deleted')
class TrashedDocumentTestCase(
TrashedDocumentTestMixin, GenericDocumentViewTestCase
):
def test_document_restore_get_view_no_permission(self): def test_document_restore_get_view_no_permission(self):
self.test_document.delete() self.test_document.delete()
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
@@ -43,13 +85,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(Document.objects.count(), document_count) self.assertEqual(Document.objects.count(), document_count)
def _request_document_restore_post_view(self):
return self.post(
viewname='documents:document_restore', kwargs={
'pk': self.test_document.pk
}
)
def test_document_restore_post_view_no_permission(self): def test_document_restore_post_view_no_permission(self):
self.test_document.delete() self.test_document.delete()
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
@@ -74,13 +109,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(DeletedDocument.objects.count(), 0) self.assertEqual(DeletedDocument.objects.count(), 0)
self.assertEqual(Document.objects.count(), 1) self.assertEqual(Document.objects.count(), 1)
def _request_document_trash_get_view(self):
return self.get(
viewname='documents:document_trash', kwargs={
'pk': self.test_document.pk
}
)
def test_document_trash_get_view_no_permissions(self): def test_document_trash_get_view_no_permissions(self):
document_count = Document.objects.count() document_count = Document.objects.count()
@@ -101,13 +129,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(Document.objects.count(), document_count) self.assertEqual(Document.objects.count(), document_count)
def _request_document_trash_post_view(self):
return self.post(
viewname='documents:document_trash', kwargs={
'pk': self.test_document.pk
}
)
def test_document_trash_post_view_no_permissions(self): def test_document_trash_post_view_no_permissions(self):
response = self._request_document_trash_post_view() response = self._request_document_trash_post_view()
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@@ -126,13 +147,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(DeletedDocument.objects.count(), 1) self.assertEqual(DeletedDocument.objects.count(), 1)
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
def _request_document_delete_get_view(self):
return self.get(
viewname='documents:document_delete', kwargs={
'pk': self.test_document.pk
}
)
def test_document_delete_get_view_no_permissions(self): def test_document_delete_get_view_no_permissions(self):
self.test_document.delete() self.test_document.delete()
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
@@ -140,7 +154,7 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
trashed_document_count = DeletedDocument.objects.count() trashed_document_count = DeletedDocument.objects.count()
response = self._request_document_delete_get_view() response = self._request_trashed_document_delete_get_view()
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertEqual( self.assertEqual(
@@ -158,26 +172,19 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
trashed_document_count = DeletedDocument.objects.count() trashed_document_count = DeletedDocument.objects.count()
response = self._request_document_delete_get_view() response = self._request_trashed_document_delete_get_view()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual( self.assertEqual(
DeletedDocument.objects.count(), trashed_document_count DeletedDocument.objects.count(), trashed_document_count
) )
def _request_document_delete_post_view(self):
return self.post(
viewname='documents:document_delete', kwargs={
'pk': self.test_document.pk
}
)
def test_document_delete_post_view_no_permissions(self): def test_document_delete_post_view_no_permissions(self):
self.test_document.delete() self.test_document.delete()
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
self.assertEqual(DeletedDocument.objects.count(), 1) self.assertEqual(DeletedDocument.objects.count(), 1)
response = self._request_document_delete_post_view() response = self._request_trashed_document_delete_post_view()
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
@@ -192,19 +199,16 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
obj=self.test_document, permission=permission_document_delete obj=self.test_document, permission=permission_document_delete
) )
response = self._request_document_delete_post_view() response = self._request_trashed_document_delete_post_view()
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(DeletedDocument.objects.count(), 0) self.assertEqual(DeletedDocument.objects.count(), 0)
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
def _request_document_list_deleted_view(self):
return self.get(viewname='documents:document_list_deleted')
def test_deleted_document_list_view_no_permissions(self): def test_deleted_document_list_view_no_permissions(self):
self.test_document.delete() self.test_document.delete()
response = self._request_document_list_deleted_view() response = self._request_trashed_document_list_view()
self.assertNotContains( self.assertNotContains(
response=response, text=self.test_document.label, status_code=200 response=response, text=self.test_document.label, status_code=200
) )
@@ -216,7 +220,7 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
obj=self.test_document, permission=permission_document_view obj=self.test_document, permission=permission_document_view
) )
response = self._request_document_list_deleted_view() response = self._request_trashed_document_list_view()
self.assertContains( self.assertContains(
response=response, text=self.test_document.label, status_code=200 response=response, text=self.test_document.label, status_code=200
) )

View File

@@ -4,21 +4,24 @@ from django.conf.urls import url
from .api_views import ( from .api_views import (
APITrashedDocumentListView, APIDeletedDocumentRestoreView, APITrashedDocumentListView, APIDeletedDocumentRestoreView,
APIDeletedDocumentView, APIDocumentDownloadView, APIDocumentView, APIDeletedDocumentView, APIDocumentDownloadView, APIDocumentPageListView,
APIDocumentListView, APIDocumentVersionDownloadView, APIDocumentView, APIDocumentListView, APIDocumentVersionDownloadView,
APIDocumentPageImageView, APIDocumentPageView, APIDocumentPageImageView, APIDocumentPageView,
APIDocumentTypeDocumentListView, APIDocumentTypeListView, APIDocumentTypeDocumentListView, APIDocumentTypeListView,
APIDocumentTypeView, APIDocumentVersionsListView, APIDocumentTypeView, APIDocumentVersionsListView,
APIDocumentVersionPageListView, APIDocumentVersionView, APIDocumentVersionPageListView, APIDocumentVersionView,
APIRecentDocumentListView APIRecentDocumentListView,
APIDocumentVersionPageView,
APIDocumentVersionPageImageView
) )
from .views.document_views import ( from .views.document_views import (
DocumentDocumentTypeEditView, DocumentDownloadFormView, DocumentDocumentTypeEditView, DocumentDownloadFormView,
DocumentDownloadView, DocumentDuplicatesListView, DocumentEditView, DocumentDownloadView, DocumentDuplicatesListView, DocumentEditView,
DocumentListView, DocumentPreviewView, DocumentPrint, DocumentListView, DocumentPreviewView, DocumentPrint,
DocumentTransformationsClearView, DocumentTransformationsCloneView, DocumentPagesResetView, DocumentTransformationsClearView,
DocumentUpdatePageCountView, DocumentView, DuplicatedDocumentListView, DocumentTransformationsCloneView, DocumentView,
RecentAccessDocumentListView, RecentAddedDocumentListView DuplicatedDocumentListView, RecentAccessDocumentListView,
RecentAddedDocumentListView
) )
from .views.document_page_views import ( from .views.document_page_views import (
DocumentPageDisable, DocumentPageEnable, DocumentPageListView, DocumentPageDisable, DocumentPageEnable, DocumentPageListView,
@@ -30,7 +33,8 @@ from .views.document_page_views import (
) )
from .views.document_version_views import ( from .views.document_version_views import (
DocumentVersionDownloadFormView, DocumentVersionDownloadView, DocumentVersionDownloadFormView, DocumentVersionDownloadView,
DocumentVersionListView, DocumentVersionRevertView, DocumentVersionView, DocumentVersionListView, DocumentVersionRevertView,
DocumentVersionUpdatePageCountView, DocumentVersionView,
) )
from .views.document_type_views import ( from .views.document_type_views import (
DocumentTypeCreateView, DocumentTypeDeleteView, DocumentTypeCreateView, DocumentTypeDeleteView,
@@ -172,14 +176,14 @@ urlpatterns_documents = [
name='document_print' name='document_print'
), ),
url( url(
regex=r'^documents/(?P<pk>\d+)/reset_page_count/$', regex=r'^documents/(?P<pk>\d+)/pages/reset/$',
view=DocumentUpdatePageCountView.as_view(), view=DocumentPagesResetView.as_view(),
name='document_update_page_count' name='document_pages_reset'
), ),
url( url(
regex=r'^documents/multiple/reset_page_count/$', regex=r'^documents/multiple/pages/reset/$',
view=DocumentUpdatePageCountView.as_view(), view=DocumentPagesResetView.as_view(),
name='document_multiple_update_page_count' name='document_multiple_pages_reset'
), ),
url( url(
regex=r'^documents/(?P<pk>\d+)/download/form/$', regex=r'^documents/(?P<pk>\d+)/download/form/$',
@@ -305,6 +309,16 @@ urlpatterns_document_versions = [
view=DocumentVersionDownloadView.as_view(), view=DocumentVersionDownloadView.as_view(),
name='document_version_download' name='document_version_download'
), ),
url(
regex=r'^documents/versions/(?P<pk>\d+)/pages/update/$',
view=DocumentVersionUpdatePageCountView.as_view(),
name='document_version_page_count_update'
),
url(
regex=r'^documents/versions/multiple/pages/update/$',
view=DocumentVersionUpdatePageCountView.as_view(),
name='document_version_multiple_page_count_update'
),
url( url(
regex=r'^documents/versions/(?P<pk>\d+)/revert/$', regex=r'^documents/versions/(?P<pk>\d+)/revert/$',
view=DocumentVersionRevertView.as_view(), view=DocumentVersionRevertView.as_view(),
@@ -405,6 +419,11 @@ api_urls = [
view=APIDocumentVersionPageListView.as_view(), view=APIDocumentVersionPageListView.as_view(),
name='documentversion-page-list' name='documentversion-page-list'
), ),
url(
regex=r'^documents/(?P<pk>[0-9]+)/pages/$',
view=APIDocumentPageListView.as_view(),
name='document-page-list'
),
url( url(
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/download/$', regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/download/$',
view=APIDocumentVersionDownloadView.as_view(), view=APIDocumentVersionDownloadView.as_view(),
@@ -416,12 +435,20 @@ api_urls = [
), ),
url( url(
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)$', regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)$',
view=APIDocumentVersionPageView.as_view(), name='documentversionpage-detail'
),
url(
regex=r'^documents/(?P<pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)$',
view=APIDocumentPageView.as_view(), name='documentpage-detail' view=APIDocumentPageView.as_view(), name='documentpage-detail'
), ),
url( url(
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)/image/$', regex=r'^documents/(?P<pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)/image/$',
view=APIDocumentPageImageView.as_view(), name='documentpage-image' view=APIDocumentPageImageView.as_view(), name='documentpage-image'
), ),
url(
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)/image/$',
view=APIDocumentVersionPageImageView.as_view(), name='documentversionpage-image'
),
url( url(
regex=r'^trashed_documents/$', regex=r'^trashed_documents/$',
view=APITrashedDocumentListView.as_view(), name='trasheddocument-list' view=APITrashedDocumentListView.as_view(), name='trasheddocument-list'

View File

@@ -20,7 +20,7 @@ from mayan.apps.converter.literals import DEFAULT_ROTATION, DEFAULT_ZOOM_LEVEL
from ..forms import DocumentPageForm from ..forms import DocumentPageForm
from ..icons import icon_document_pages from ..icons import icon_document_pages
from ..links import link_document_update_page_count from ..links import link_document_pages_reset
from ..models import Document, DocumentPage from ..models import Document, DocumentPage
from ..permissions import permission_document_edit, permission_document_view from ..permissions import permission_document_edit, permission_document_view
from ..settings import ( from ..settings import (
@@ -50,13 +50,13 @@ class DocumentPageListView(ExternalObjectMixin, SingleObjectListView):
'hide_object': True, 'hide_object': True,
'list_as_items': True, 'list_as_items': True,
'no_results_icon': icon_document_pages, 'no_results_icon': icon_document_pages,
'no_results_main_link': link_document_update_page_count.resolve( 'no_results_main_link': link_document_pages_reset.resolve(
request=self.request, resolved_object=self.external_object request=self.request, resolved_object=self.external_object
), ),
'no_results_text': _( 'no_results_text': _(
'This could mean that the document is of a format that is ' 'This could mean that the document is of a format that is '
'not supported, that it is corrupted or that the upload ' 'not supported, that it is corrupted, or that the upload '
'process was interrupted. Use the document page recalculation ' 'process was interrupted. Use the document page reset '
'action to attempt to introspect the page count again.' 'action to attempt to introspect the page count again.'
), ),
'no_results_title': _('No document pages available'), 'no_results_title': _('No document pages available'),

View File

@@ -3,10 +3,11 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _, ungettext
from mayan.apps.common.generics import ( from mayan.apps.common.generics import (
ConfirmView, SingleObjectDetailView, SingleObjectListView ConfirmView, MultipleObjectConfirmActionView, SingleObjectDetailView,
SingleObjectListView
) )
from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.common.mixins import ExternalObjectMixin
@@ -14,9 +15,10 @@ from ..events import event_document_view
from ..forms import DocumentVersionDownloadForm, DocumentVersionPreviewForm from ..forms import DocumentVersionDownloadForm, DocumentVersionPreviewForm
from ..models import Document, DocumentVersion from ..models import Document, DocumentVersion
from ..permissions import ( from ..permissions import (
permission_document_download, permission_document_version_revert, permission_document_download, permission_document_tools,
permission_document_version_view permission_document_version_revert, permission_document_version_view
) )
from ..tasks import task_update_page_count
from .document_views import DocumentDownloadFormView, DocumentDownloadView from .document_views import DocumentDownloadFormView, DocumentDownloadView
@@ -142,6 +144,45 @@ class DocumentVersionRevertView(ExternalObjectMixin, ConfirmView):
) )
class DocumentVersionUpdatePageCountView(MultipleObjectConfirmActionView):
model = DocumentVersion
object_permission = permission_document_tools
success_message = _(
'%(count)d document version queued for page count recalculation'
)
success_message_plural = _(
'%(count)d documents version queued for page count recalculation'
)
def get_extra_context(self):
queryset = self.object_list
result = {
'title': ungettext(
singular='Recalculate the page count of the selected document version?',
plural='Recalculate the page count of the selected document versions?',
number=queryset.count()
)
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Recalculate the page count of the document version: %s?'
) % queryset.first()
}
)
return result
def object_action(self, form, instance):
task_update_page_count.apply_async(
kwargs={'version_id': instance.pk}
)
class DocumentVersionView(SingleObjectDetailView): class DocumentVersionView(SingleObjectDetailView):
form_class = DocumentVersionPreviewForm form_class = DocumentVersionPreviewForm
model = DocumentVersion model = DocumentVersion

View File

@@ -44,14 +44,14 @@ from ..permissions import (
from ..settings import ( from ..settings import (
setting_print_width, setting_print_height, setting_recent_added_count setting_print_width, setting_print_height, setting_recent_added_count
) )
from ..tasks import task_update_page_count from ..tasks import task_document_pages_reset
from ..utils import parse_range from ..utils import parse_range
__all__ = ( __all__ = (
'DocumentListView', 'DocumentDocumentTypeEditView', 'DocumentListView', 'DocumentDocumentTypeEditView',
'DocumentDuplicatesListView', 'DocumentEditView', 'DocumentPreviewView', 'DocumentDuplicatesListView', 'DocumentEditView', 'DocumentPreviewView',
'DocumentView', 'DocumentDownloadFormView', 'DocumentDownloadView', 'DocumentView', 'DocumentDownloadFormView', 'DocumentDownloadView',
'DocumentUpdatePageCountView', 'DocumentTransformationsClearView', 'DocumentPagesResetView', 'DocumentTransformationsClearView',
'DocumentTransformationsCloneView', 'DocumentPrint', 'DocumentTransformationsCloneView', 'DocumentPrint',
'DuplicatedDocumentListView', 'RecentAccessDocumentListView', 'DuplicatedDocumentListView', 'RecentAccessDocumentListView',
'RecentAddedDocumentListView' 'RecentAddedDocumentListView'
@@ -418,6 +418,52 @@ class DocumentPreviewView(SingleObjectDetailView):
} }
class DocumentPagesResetView(MultipleObjectConfirmActionView):
model = Document
object_permission = permission_document_tools
success_message = _('%(count)d document queued for pages reset')
success_message_plural = _('%(count)d documents queued for pages reset')
def get_extra_context(self):
queryset = self.object_list
result = {
'title': ungettext(
singular='Reset the pages of the selected document?',
plural='Reset the pages of the selected documents?',
number=queryset.count()
)
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Reset the pages of the document: %s?'
) % queryset.first()
}
)
return result
def object_action(self, form, instance):
latest_version = instance.latest_version
if latest_version:
task_document_pages_reset.apply_async(
kwargs={'document_id': instance.pk}
)
else:
messages.error(
self.request, _(
'Document "%(document)s" is empty. Upload at least one '
'document version before attempting to reset the pages. '
) % {
'document': instance,
}
)
class DocumentView(SingleObjectDetailView): class DocumentView(SingleObjectDetailView):
form_class = DocumentPropertiesForm form_class = DocumentPropertiesForm
model = Document model = Document
@@ -436,57 +482,6 @@ class DocumentView(SingleObjectDetailView):
} }
class DocumentUpdatePageCountView(MultipleObjectConfirmActionView):
model = Document
object_permission = permission_document_tools
success_message = _(
'%(count)d document queued for page count recalculation'
)
success_message_plural = _(
'%(count)d documents queued for page count recalculation'
)
def get_extra_context(self):
queryset = self.object_list
result = {
'title': ungettext(
singular='Recalculate the page count of the selected document?',
plural='Recalculate the page count of the selected documents?',
number=queryset.count()
)
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Recalculate the page count of the document: %s?'
) % queryset.first()
}
)
return result
def object_action(self, form, instance):
latest_version = instance.latest_version
if latest_version:
task_update_page_count.apply_async(
kwargs={'version_id': latest_version.pk}
)
else:
messages.error(
self.request, _(
'Document "%(document)s" is empty. Upload at least one '
'document version before attempting to detect the '
'page count.'
) % {
'document': instance,
}
)
class DocumentTransformationsClearView(MultipleObjectConfirmActionView): class DocumentTransformationsClearView(MultipleObjectConfirmActionView):
model = Document model = Document
object_permission = permission_transformation_delete object_permission = permission_transformation_delete

View File

@@ -184,7 +184,14 @@ class SearchModel(object):
query_string=query_string, global_and_search=global_and_search query_string=query_string, global_and_search=global_and_search
) )
try:
queryset = self.get_queryset().filter(search_query.query).distinct() queryset = self.get_queryset().filter(search_query.query).distinct()
except Exception:
logger.error(
'Error filtering model %s with queryset: %s', self.model,
search_query.query
)
raise
if self.permission: if self.permission:
queryset = AccessControlList.objects.restrict_queryset( queryset = AccessControlList.objects.restrict_queryset(

View File

@@ -0,0 +1,17 @@
from __future__ import unicode_literals
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
namespace = Namespace(label=_('Search'), name='search')
setting_disable_simple_search = namespace.add_setting(
global_name='SEARCH_DISABLE_SIMPLE_SEARCH',
default=False, help_text=_(
'Disables the single term bar search leaving only the advanced '
'search button.'
)
)

View File

@@ -1,13 +1,25 @@
{% load i18n %} {% load i18n %}
{% load search_tags %} {% load search_tags %}
{% load smart_settings_tags %}
{% get_search_models as search_models %} {% get_search_models as search_models %}
{% smart_setting global_name="SEARCH_DISABLE_SIMPLE_SEARCH" as setting_disable_simple_search %}
{% if setting_disable_simple_search %}
<div class="row">
<div class="col-xs-6 col-xs-offset-3">
{% endif %}
<div class="well center-block"> <div class="well center-block">
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<form action="{% url 'search:results' %}" class="form-horizontal" id="formSearch" method="get" role="search"> <form action="{% url 'search:results' %}" class="form-horizontal" id="formSearch" method="get" role="search">
{% if setting_disable_simple_search == False %}
<div class="col-sm-2"> <div class="col-sm-2">
{% else %}
<div class="col-sm-8">
{% endif %}
<select class="form-control" id="selectSearchModel" name="_search_model"> <select class="form-control" id="selectSearchModel" name="_search_model">
{% for search_model in search_models %} {% for search_model in search_models %}
{{ search_model.self.get_full_name }} {{ search_model.self.get_full_name }}
@@ -16,12 +28,21 @@
</select> </select>
</div> </div>
{% if setting_disable_simple_search == False %}
<div class="col-sm-10"> <div class="col-sm-10">
{% else %}
<div class="col-sm-4">
{% endif %}
<div class="input-group"> <div class="input-group">
{% if setting_disable_simple_search == False %}
<input class="form-control" name="q" placeholder="{% trans 'Search terms' %}" type="text" value="{{ search_terms|default:'' }}"> <input class="form-control" name="q" placeholder="{% trans 'Search terms' %}" type="text" value="{{ search_terms|default:'' }}">
{% endif %}
<span class="input-group-btn"> <span class="input-group-btn">
{% if setting_disable_simple_search == False %}
<button class="btn btn-default" type="submit">{% trans 'Search' %}</button> <button class="btn btn-default" type="submit">{% trans 'Search' %}</button>
<a class="btn btn-primary" href="" id="btnSearchAdvanced" >{% trans 'Advanced' %}</a> {% endif %}
<a class="btn btn-primary" href="" id="btnSearchAdvanced" > {% if setting_disable_simple_search == False %}{% trans 'Advanced' %}{% else %}{% trans 'Advanced search' %}{% endif %}</a>
</span> </span>
</div> </div>
</div> </div>
@@ -30,6 +51,12 @@
</div> </div>
</div> </div>
{% if setting_disable_simple_search %}
</div>
</div>
{% endif %}
<script> <script>
jQuery(document).ready(function() { jQuery(document).ready(function() {
var $selectSearchModel = $('#selectSearchModel'); var $selectSearchModel = $('#selectSearchModel');

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.apps import apps from django.apps import apps
from django.db.models.signals import post_migrate
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.apps import MayanAppConfig from mayan.apps.common.apps import MayanAppConfig
@@ -11,6 +12,7 @@ from mayan.apps.common.menus import (
from mayan.apps.navigation.classes import SourceColumn from mayan.apps.navigation.classes import SourceColumn
from .dependencies import * # NOQA from .dependencies import * # NOQA
from .handlers import handler_create_system_user
from .html_widgets import ( from .html_widgets import (
ObjectLinkWidget, widget_event_actor_link, widget_event_type_link ObjectLinkWidget, widget_event_actor_link, widget_event_type_link
) )
@@ -101,3 +103,8 @@ class EventsApp(MayanAppConfig):
link_event_types_subscriptions_list, link_current_user_events link_event_types_subscriptions_list, link_current_user_events
), position=50 ), position=50
) )
post_migrate.connect(
dispatch_uid='events_create_system_user',
receiver=handler_create_system_user,
)

View File

@@ -0,0 +1,7 @@
from __future__ import unicode_literals
from .utils import create_system_user
def handler_create_system_user(sender, **kwargs):
create_system_user()

View File

@@ -0,0 +1,23 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.auth import get_user_model
def create_system_user():
"""
User account without a password used to attach events that normally
won't have an actor and a target
"""
user, created = get_user_model().objects.get_or_create(
username='system', defaults={
'first_name': 'System', 'is_staff': False
}
)
return user
def get_system_user():
user = get_user_model().objects.get(username='system')
return user

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-10-08 15:10
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('file_caching', '0002_auto_20190729_0236'),
]
operations = [
migrations.AlterField(
model_name='cache',
name='name',
field=models.CharField(db_index=True, help_text='Internal name of the cache.', max_length=128, unique=True, verbose_name='Name'),
),
]

View File

@@ -12,7 +12,9 @@ from mayan.apps.common.menus import (
menu_tools menu_tools
) )
from mayan.apps.document_indexing.handlers import handler_index_document from mayan.apps.document_indexing.handlers import handler_index_document
from mayan.apps.documents.search import document_page_search, document_search from mayan.apps.documents.search import (
document_page_search, document_search, document_version_page_search
)
from mayan.apps.documents.signals import post_version_upload from mayan.apps.documents.signals import post_version_upload
from mayan.apps.events.classes import ModelEventType from mayan.apps.events.classes import ModelEventType
from mayan.apps.navigation.classes import SourceColumn from mayan.apps.navigation.classes import SourceColumn
@@ -156,11 +158,19 @@ class FileMetadataApp(MayanAppConfig):
label=_('File metadata value') label=_('File metadata value')
) )
document_page_search.add_model_field( #document_page_search.add_model_field(
# field='document__document_version__file_metadata_drivers__entries__key',
# label=_('File metadata key')
#)
#document_page_search.add_model_field(
# field='document__document_version__file_metadata_drivers__entries__value',
# label=_('File metadata value')
#)
document_version_page_search.add_model_field(
field='document_version__file_metadata_drivers__entries__key', field='document_version__file_metadata_drivers__entries__key',
label=_('File metadata key') label=_('File metadata key')
) )
document_page_search.add_model_field( document_version_page_search.add_model_field(
field='document_version__file_metadata_drivers__entries__value', field='document_version__file_metadata_drivers__entries__value',
label=_('File metadata value') label=_('File metadata value')
) )

View File

@@ -0,0 +1,3 @@
from __future__ import unicode_literals
default_app_config = 'mayan.apps.importer.apps.ImporterApp'

View File

@@ -0,0 +1,17 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.apps import MayanAppConfig
class ImporterApp(MayanAppConfig):
app_namespace = 'importer'
app_url = 'importer'
has_rest_api = False
has_tests = True
name = 'mayan.apps.importer'
verbose_name = _('Importer')
def ready(self):
super(ImporterApp, self).ready()

View File

@@ -0,0 +1,152 @@
from __future__ import unicode_literals
import csv
import time
from django.apps import apps
from django.core import management
from django.core.files import File
from ...tasks import task_upload_new_document
class Command(management.BaseCommand):
help = 'Import documents from a CSV file.'
def add_arguments(self, parser):
parser.add_argument(
'--document_type_column',
action='store', dest='document_type_column', default=0,
help='Column that contains the document type labels. Column '
'numbers start at 0.',
type=int
)
parser.add_argument(
'--document_path_column',
action='store', dest='document_path_column', default=1,
help='Column that contains the path to the document files. Column '
'numbers start at 0.',
type=int
)
parser.add_argument(
'--ignore_errors',
action='store_true', dest='ignore_errors', default=False,
help='Don\'t stop the import process on common errors like '
'incorrect file paths.',
)
parser.add_argument(
'--ignore_rows',
action='store', dest='ignore_rows', default='',
help='Ignore a set of rows. Row numbers must be separated by commas.'
)
parser.add_argument(
'--metadata_pairs_column',
action='store', dest='metadata_pairs_column',
help='Column that contains metadata name and values for the '
'documents. Use the form: <label column>:<value column>. Example: '
'2:5. Separate multiple pairs with commas. Example: 2:5,7:10',
)
parser.add_argument('filelist', nargs='?', help='File list')
def handle(self, *args, **options):
time_start = time.time()
time_last_display = time_start
document_types = {}
uploaded_count = 0
row_count = 0
rows_to_ignore = []
for entry in options['ignore_rows'].split(','):
if entry:
rows_to_ignore.append(int(entry))
DocumentType = apps.get_model(
app_label='documents', model_name='DocumentType'
)
SharedUploadedFile = apps.get_model(
app_label='common', model_name='SharedUploadedFile'
)
if not options['filelist']:
self.stderr.write('Must specify a CSV file path.')
exit(1)
else:
with open(options['filelist'], mode='r') as csv_datafile:
csv_reader = csv.reader(
csv_datafile, delimiter=',', quotechar='"'
)
for row in csv_reader:
# Increase row count here even though start index is 0
# purpose is to avoid losing row number increments on
# exceptions
row_count = row_count + 1
if row_count - 1 not in rows_to_ignore:
try:
with open(row[options['document_path_column']], mode='rb') as file_object:
document_type_label = row[options['document_type_column']]
if document_type_label not in document_types:
self.stdout.write(
'New document type: {}. Creating and caching.'.format(
document_type_label
)
)
document_type, created = DocumentType.objects.get_or_create(
label=document_type_label
)
document_types[document_type_label] = document_type
else:
document_type = document_types[document_type_label]
shared_uploaded_file = SharedUploadedFile.objects.create(
file=File(file_object)
)
extra_data = {}
if options['metadata_pairs_column']:
extra_data['metadata_pairs'] = []
for pair in options['metadata_pairs_column'].split(','):
name, value = pair.split(':')
extra_data['metadata_pairs'].append(
{
'name': row[int(name)],
'value': row[int(value)]
}
)
task_upload_new_document.apply_async(
kwargs=dict(
document_type_id=document_type.pk,
shared_uploaded_file_id=shared_uploaded_file.pk,
extra_data=extra_data
)
)
uploaded_count = uploaded_count + 1
if (time.time() - time_last_display) > 1:
time_last_display = time.time()
self.stdout.write(
'Time: {}s, Files copied and queued: {}, files processed per second: {}'.format(
int(time.time() - time_start),
uploaded_count,
uploaded_count / (time.time() - time_start)
)
)
except (IOError, OSError) as exception:
if not options['ignore_errors']:
raise
else:
self.stderr.write(
'Error processing row: {}; {}.'.format(
row_count - 1, exception
)
)
self.stdout.write(
'Total files copied and queues: {}'.format(uploaded_count)
)
self.stdout.write(
'Total time: {}'.format(time.time() - time_start)
)

View File

@@ -0,0 +1,10 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.documents.queues import queue_uploads
queue_uploads.add_task_type(
dotted_path='mayan.apps.importer.tasks.task_upload_new_document',
label=_('Import new document')
)

View File

@@ -0,0 +1,93 @@
from __future__ import unicode_literals
import logging
from django.apps import apps
from django.db import OperationalError
from django.utils.text import slugify
from mayan.celery import app
from mayan.apps.documents.literals import UPLOAD_NEW_DOCUMENT_RETRY_DELAY
logger = logging.getLogger(__name__)
@app.task(bind=True, default_retry_delay=UPLOAD_NEW_DOCUMENT_RETRY_DELAY, ignore_result=True)
def task_upload_new_document(self, document_type_id, shared_uploaded_file_id, extra_data=None):
DocumentType = apps.get_model(
app_label='documents', model_name='DocumentType'
)
MetadataType = apps.get_model(
app_label='metadata', model_name='MetadataType'
)
SharedUploadedFile = apps.get_model(
app_label='common', model_name='SharedUploadedFile'
)
try:
document_type = DocumentType.objects.get(pk=document_type_id)
shared_file = SharedUploadedFile.objects.get(
pk=shared_uploaded_file_id
)
except OperationalError as exception:
logger.warning(
'Operational error during attempt to retrieve shared data for '
'new document of type ID: %d; %s. Retrying.', document_type_id,
exception
)
raise self.retry(exc=exception)
try:
with shared_file.open() as file_object:
new_document = document_type.new_document(file_object=file_object)
except OperationalError as exception:
logger.warning(
'Operational error during attempt to create new document '
'of type: %s; %s. Retrying.', document_type, exception
)
raise self.retry(exc=exception)
except Exception as exception:
# This except and else block emulate a finally:
logger.error(
'Unexpected error during attempt to create new document '
'of type: %s; %s', document_type, exception
)
try:
shared_file.delete()
except OperationalError as exception:
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)
else:
if extra_data:
for pair in extra_data.get('metadata_pairs', []):
name = slugify(pair['name']).replace('-', '_')
logger.debug(
'Metadata pair (label, name, value): %s, %s, %s',
pair['name'], name, pair['value']
)
metadata_type, created = MetadataType.objects.get_or_create(
name=name, defaults={'label': pair['name']}
)
if not new_document.document_type.metadata.filter(metadata_type=metadata_type).exists():
logger.debug('Metadata type created')
new_document.document_type.metadata.create(
metadata_type=metadata_type, required=False
)
new_document.metadata.create(
metadata_type=metadata_type, value=pair['value']
)
try:
shared_file.delete()
except OperationalError as exception:
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)

View File

View File

@@ -0,0 +1,117 @@
from __future__ import unicode_literals
import csv
from django.core import management
from django.utils.encoding import force_bytes, force_text
from mayan.apps.documents.models import DocumentType, Document
from mayan.apps.documents.tests.base import GenericDocumentTestCase
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
from mayan.apps.storage.utils import fs_cleanup, mkstemp
class ImportManagementCommandTestCase(GenericDocumentTestCase):
auto_generate_test_csv_file = True
auto_upload_document = False
random_primary_key_enable = False
test_import_count = 1
def setUp(self):
super(ImportManagementCommandTestCase, self).setUp()
if self.auto_generate_test_csv_file:
self._create_test_csv_file()
def tearDown(self):
self._destroy_test_csv_file()
super(ImportManagementCommandTestCase, self).tearDown()
def _create_test_csv_file(self):
self.test_csv_path = mkstemp()[1]
print('Test CSV file: {}'.format(self.test_csv_path))
with open(self.test_csv_path, mode='w', newline='') as file_object:
filewriter = csv.writer(
file_object, delimiter=',', quotechar='"',
quoting=csv.QUOTE_MINIMAL
)
print(
'Generating test CSV for {} documents'.format(
self.test_import_count
)
)
for times in range(self.test_import_count):
filewriter.writerow(
[
self.test_document_type.label, TEST_SMALL_DOCUMENT_PATH,
'column 2', 'column 3', 'column 4', 'column 5',
'part #', 'value',
]
)
filewriter.writerow(
[
self.test_document_type.label, TEST_SMALL_DOCUMENT_PATH,
'column 2', 'column 3', 'column 4', 'column 5',
'part#', 'value',
]
)
def _destroy_test_csv_file(self):
fs_cleanup(filename=self.test_csv_path)
def test_import_csv_read(self):
self.test_document_type.delete()
management.call_command('import', self.test_csv_path)
self.assertTrue(DocumentType.objects.count() > 0)
self.assertTrue(Document.objects.count() > 0)
def test_import_document_type_column_mapping(self):
self.test_document_type.delete()
management.call_command(
'import', self.test_csv_path, '--document_type_column', '2'
)
self.assertTrue(DocumentType.objects.first().label == 'column 2')
self.assertTrue(Document.objects.count() > 0)
def test_import_document_path_column_mapping(self):
self.test_document_type.delete()
with self.assertRaises(IOError):
management.call_command(
'import', self.test_csv_path, '--document_path_column', '2'
)
def test_import_metadata_column_mapping(self):
self.test_document_type.delete()
management.call_command(
'import', self.test_csv_path, '--metadata_pairs_column', '2:3,4:5',
)
self.assertTrue(DocumentType.objects.count() > 0)
self.assertTrue(Document.objects.count() > 0)
self.assertTrue(Document.objects.first().metadata.count() > 0)
self.assertEqual(
Document.objects.first().metadata.get(
metadata_type__name='column_2'
).value, 'column 3'
)
def test_import_ambiguous_metadata(self):
self.auto_generate_test_csv_file = False
self.test_import_count = 2
self.test_document_type.delete()
management.call_command(
'import', self.test_csv_path, '--metadata_pairs_column', '6:7',
)
self.assertTrue(DocumentType.objects.count() > 0)
self.assertTrue(Document.objects.count() > 0)
self.assertTrue(Document.objects.first().metadata.count() > 0)
self.assertEqual(
Document.objects.first().metadata.get(
metadata_type__name='part'
).value, 'value'
)

View File

@@ -16,7 +16,9 @@ from mayan.apps.common.menus import (
menu_facet, menu_list_facet, menu_multi_item, menu_object, menu_secondary, menu_facet, menu_list_facet, menu_multi_item, menu_object, menu_secondary,
menu_setup menu_setup
) )
from mayan.apps.documents.search import document_page_search, document_search from mayan.apps.documents.search import (
document_page_search, document_search, document_version_page_search
)
from mayan.apps.documents.signals import post_document_type_change from mayan.apps.documents.signals import post_document_type_change
from mayan.apps.events.classes import ModelEventType from mayan.apps.events.classes import ModelEventType
from mayan.apps.events.links import ( from mayan.apps.events.links import (
@@ -76,7 +78,7 @@ class MetadataApp(MayanAppConfig):
app_label='documents', model_name='Document' app_label='documents', model_name='Document'
) )
DocumentPageResult = apps.get_model( DocumentPageResult = apps.get_model(
app_label='documents', model_name='DocumentPageResult' app_label='documents', model_name='DocumentVersionPageResult'
) )
DocumentType = apps.get_model( DocumentType = apps.get_model(
@@ -188,10 +190,18 @@ class MetadataApp(MayanAppConfig):
) )
document_page_search.add_model_field( document_page_search.add_model_field(
field='document_version__document__metadata__metadata_type__name', field='document__metadata__metadata_type__name',
label=_('Metadata type') label=_('Metadata type')
) )
document_page_search.add_model_field( document_page_search.add_model_field(
field='document__metadata__value',
label=_('Metadata value')
)
document_version_page_search.add_model_field(
field='document_version__document__metadata__metadata_type__name',
label=_('Metadata type')
)
document_version_page_search.add_model_field(
field='document_version__document__metadata__value', field='document_version__document__metadata__value',
label=_('Metadata value') label=_('Metadata value')
) )

View File

@@ -689,7 +689,7 @@ class SourceColumn(object):
logger.warning( logger.warning(
'No request variable, aborting request resolution' 'No request variable, aborting request resolution'
) )
return result return final_result
current_view_name = get_current_view_name(request=request) current_view_name = get_current_view_name(request=request)
for column in columns: for column in columns:

View File

@@ -108,12 +108,6 @@ def navigation_resolve_menus(context, names, source=None, sort_results=None):
return result return result
@register.simple_tag(takes_context=True)
def resolve_link(context, link):
# This can be used to resolve links or menus too
return link.resolve(context=context)
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def navigation_source_column_resolve(context, column): def navigation_source_column_resolve(context, column):
if column: if column:
@@ -121,3 +115,9 @@ def navigation_source_column_resolve(context, column):
return result return result
else: else:
return '' return ''
@register.simple_tag(takes_context=True)
def resolve_link(context, link):
# This can be used to resolve links or menus too
return link.resolve(context=context)

View File

@@ -3,13 +3,15 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
DocumentPageOCRContent, DocumentTypeSettings, DocumentVersionOCRError DocumentTypeSettings, DocumentVersionPageOCRContent,
DocumentVersionOCRError
) )
@admin.register(DocumentPageOCRContent) @admin.register(DocumentVersionPageOCRContent)
class DocumentPageOCRContentAdmin(admin.ModelAdmin): class DocumentVersionPageOCRContentAdmin(admin.ModelAdmin):
list_display = ('document_page',) pass
#list_display = ('document_page',)
@admin.register(DocumentTypeSettings) @admin.register(DocumentTypeSettings)

View File

@@ -8,7 +8,7 @@ from rest_framework.response import Response
from mayan.apps.documents.models import Document, DocumentVersion from mayan.apps.documents.models import Document, DocumentVersion
from mayan.apps.rest_api.permissions import MayanPermission from mayan.apps.rest_api.permissions import MayanPermission
from .models import DocumentPageOCRContent from .models import DocumentVersionPageOCRContent
from .permissions import permission_ocr_content_view, permission_ocr_document from .permissions import permission_ocr_content_view, permission_ocr_document
from .serializers import DocumentPageOCRContentSerializer from .serializers import DocumentPageOCRContentSerializer
@@ -90,8 +90,8 @@ class APIDocumentPageOCRContentView(generics.RetrieveAPIView):
try: try:
ocr_content = instance.ocr_content ocr_content = instance.ocr_content
except DocumentPageOCRContent.DoesNotExist: except DocumentVersionPageOCRContent.DoesNotExist:
ocr_content = DocumentPageOCRContent.objects.none() ocr_content = DocumentVersionPageOCRContent.objects.none()
serializer = self.get_serializer(ocr_content) serializer = self.get_serializer(ocr_content)
return Response(serializer.data) return Response(serializer.data)

View File

@@ -12,7 +12,9 @@ from mayan.apps.common.classes import ModelField
from mayan.apps.common.menus import ( from mayan.apps.common.menus import (
menu_facet, menu_list_facet, menu_multi_item, menu_secondary, menu_tools menu_facet, menu_list_facet, menu_multi_item, menu_secondary, menu_tools
) )
from mayan.apps.documents.search import document_search, document_page_search from mayan.apps.documents.search import (
document_search, document_page_search, document_version_page_search
)
from mayan.apps.documents.signals import post_version_upload from mayan.apps.documents.signals import post_version_upload
from mayan.apps.events.classes import ModelEventType from mayan.apps.events.classes import ModelEventType
from mayan.apps.navigation.classes import SourceColumn from mayan.apps.navigation.classes import SourceColumn
@@ -32,17 +34,19 @@ from .links import (
link_document_ocr_content_delete_multiple, link_document_ocr_download, link_document_ocr_content_delete_multiple, link_document_ocr_download,
link_document_ocr_errors_list, link_document_submit, link_document_ocr_errors_list, link_document_submit,
link_document_submit_multiple, link_document_type_ocr_settings, link_document_submit_multiple, link_document_type_ocr_settings,
link_document_type_submit, link_entry_list link_document_type_submit, link_document_version_page_ocr_content,
link_entry_list
) )
from .methods import ( from .methods import (
method_document_ocr_submit, method_document_version_ocr_submit method_document_ocr_submit, method_document_page_get_ocr_content,
method_document_version_ocr_submit
) )
from .permissions import ( from .permissions import (
permission_document_type_ocr_setup, permission_ocr_document, permission_document_type_ocr_setup, permission_ocr_document,
permission_ocr_content_view permission_ocr_content_view
) )
from .signals import post_document_version_ocr from .signals import post_document_version_ocr
from .utils import get_document_ocr_content from .utils import get_document_ocr_content, get_document_version_ocr_content
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -73,6 +77,9 @@ class OCRApp(MayanAppConfig):
DocumentVersion = apps.get_model( DocumentVersion = apps.get_model(
app_label='documents', model_name='DocumentVersion' app_label='documents', model_name='DocumentVersion'
) )
DocumentVersionPage = apps.get_model(
app_label='documents', model_name='DocumentVersionPage'
)
DocumentVersionOCRError = self.get_model( DocumentVersionOCRError = self.get_model(
model_name='DocumentVersionOCRError' model_name='DocumentVersionOCRError'
@@ -81,8 +88,11 @@ class OCRApp(MayanAppConfig):
Document.add_to_class( Document.add_to_class(
name='submit_for_ocr', value=method_document_ocr_submit name='submit_for_ocr', value=method_document_ocr_submit
) )
DocumentPage.add_to_class(
name='get_ocr_content', value=method_document_page_get_ocr_content
)
DocumentVersion.add_to_class( DocumentVersion.add_to_class(
name='ocr_content', value=get_document_ocr_content name='ocr_content', value=get_document_version_ocr_content
) )
DocumentVersion.add_to_class( DocumentVersion.add_to_class(
name='submit_for_ocr', value=method_document_version_ocr_submit name='submit_for_ocr', value=method_document_version_ocr_submit
@@ -97,7 +107,7 @@ class OCRApp(MayanAppConfig):
) )
ModelField( ModelField(
model=Document, name='versions__version_pages__ocr_content__content' model=Document, name='versions__pages__ocr_content__content'
) )
ModelPermission.register( ModelPermission.register(
@@ -128,12 +138,14 @@ class OCRApp(MayanAppConfig):
) )
document_search.add_model_field( document_search.add_model_field(
field='versions__version_pages__ocr_content__content', label=_('OCR') field='versions__pages__ocr_content__content', label=_('OCR')
) )
document_version_page_search.add_model_field(
document_page_search.add_model_field(
field='ocr_content__content', label=_('OCR') field='ocr_content__content', label=_('OCR')
) )
#document_page_search.add_model_field(
# field='ocr_content__content', label=_('OCR')
#)
menu_facet.bind_links( menu_facet.bind_links(
links=(link_document_ocr_content,), sources=(Document,) links=(link_document_ocr_content,), sources=(Document,)
@@ -141,6 +153,10 @@ class OCRApp(MayanAppConfig):
menu_list_facet.bind_links( menu_list_facet.bind_links(
links=(link_document_page_ocr_content,), sources=(DocumentPage,) links=(link_document_page_ocr_content,), sources=(DocumentPage,)
) )
menu_list_facet.bind_links(
links=(link_document_version_page_ocr_content,),
sources=(DocumentVersionPage,)
)
menu_list_facet.bind_links( menu_list_facet.bind_links(
links=(link_document_type_ocr_settings,), sources=(DocumentType,) links=(link_document_type_ocr_settings,), sources=(DocumentType,)
) )

View File

@@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext
from mayan.apps.common.widgets import TextAreaDiv from mayan.apps.common.widgets import TextAreaDiv
from .models import DocumentPageOCRContent from .models import DocumentVersionPageOCRContent
class DocumentPageOCRContentForm(forms.Form): class DocumentPageOCRContentForm(forms.Form):
@@ -28,15 +28,26 @@ class DocumentPageOCRContentForm(forms.Form):
content = '' content = ''
self.fields['contents'].initial = '' self.fields['contents'].initial = ''
try: content = conditional_escape(
page_content = page.ocr_content.content force_text(self.get_instance_ocr_content(instance=page))
except DocumentPageOCRContent.DoesNotExist: )
pass
else:
content = conditional_escape(force_text(page_content))
self.fields['contents'].initial = mark_safe(content) self.fields['contents'].initial = mark_safe(content)
def get_instance_ocr_content(self, instance):
try:
return instance.content_object.ocr_content.content
except DocumentVersionPageOCRContent.DoesNotExist:
return ''
class DocumentVersionPageOCRContentForm(DocumentPageOCRContentForm):
def get_instance_ocr_content(self, instance):
try:
return instance.ocr_content.content
except (AttributeError, DocumentVersionPageOCRContent.DoesNotExist):
return ''
class DocumentOCRContentForm(forms.Form): class DocumentOCRContentForm(forms.Form):
""" """
@@ -54,19 +65,15 @@ class DocumentOCRContentForm(forms.Form):
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.document = kwargs.pop('instance', None) document = kwargs.pop('instance', None)
super(DocumentOCRContentForm, self).__init__(*args, **kwargs) super(DocumentOCRContentForm, self).__init__(*args, **kwargs)
content = [] content = []
self.fields['contents'].initial = '' self.fields['contents'].initial = ''
try:
document_pages = self.document.pages.all()
except AttributeError:
document_pages = []
for page in document_pages: for document_page in document.pages.all():
try: try:
page_content = page.ocr_content.content page_content = document_page.content_object.ocr_content.content
except DocumentPageOCRContent.DoesNotExist: except (AttributeError, DocumentVersionPageOCRContent.DoesNotExist):
pass pass
else: else:
content.append(conditional_escape(force_text(page_content))) content.append(conditional_escape(force_text(page_content)))
@@ -74,7 +81,7 @@ class DocumentOCRContentForm(forms.Form):
'\n\n\n<hr/><div class="document-page-content-divider">- %s -</div><hr/>\n\n\n' % ( '\n\n\n<hr/><div class="document-page-content-divider">- %s -</div><hr/>\n\n\n' % (
ugettext( ugettext(
'Page %(page_number)d' 'Page %(page_number)d'
) % {'page_number': page.page_number} ) % {'page_number': document_page.page_number}
) )
) )

View File

@@ -19,7 +19,7 @@ icon_document_ocr_errors_list = Icon(
icon_document_type_ocr_settings = Icon( icon_document_type_ocr_settings = Icon(
driver_name='fontawesome', symbol='font' driver_name='fontawesome', symbol='font'
) )
icon_document_type_submit = Icon(driver_name='fontawesome', symbol='font')
icon_entry_list = Icon(driver_name='fontawesome', symbol='font')
icon_document_submit = icon_document_multiple_submit icon_document_submit = icon_document_multiple_submit
icon_document_type_submit = Icon(driver_name='fontawesome', symbol='font')
icon_document_version_page_ocr_content = Icon(driver_name='fontawesome', symbol='font')
icon_entry_list = Icon(driver_name='fontawesome', symbol='font')

View File

@@ -58,10 +58,11 @@ link_document_type_submit = Link(
permissions=(permission_ocr_document,), text=_('OCR documents per type'), permissions=(permission_ocr_document,), text=_('OCR documents per type'),
view='ocr:document_type_submit' view='ocr:document_type_submit'
) )
link_entry_list = Link( link_document_version_page_ocr_content = Link(
icon_class_path='mayan.apps.ocr.icons.icon_entry_list', args='resolved_object.id',
permissions=(permission_ocr_document,), text=_('OCR errors'), icon_class_path='mayan.apps.ocr.icons.icon_document_version_page_ocr_content',
view='ocr:entry_list' permissions=(permission_ocr_content_view,), text=_('OCR'),
view='ocr:document_version_page_ocr_content',
) )
link_document_ocr_errors_list = Link( link_document_ocr_errors_list = Link(
args='resolved_object.id', args='resolved_object.id',
@@ -75,3 +76,8 @@ link_document_ocr_download = Link(
permissions=(permission_ocr_content_view,), text=_('Download OCR text'), permissions=(permission_ocr_content_view,), text=_('Download OCR text'),
view='ocr:document_ocr_download' view='ocr:document_ocr_download'
) )
link_entry_list = Link(
icon_class_path='mayan.apps.ocr.icons.icon_entry_list',
permissions=(permission_ocr_document,), text=_('OCR errors'),
view='ocr:entry_list'
)

View File

@@ -9,7 +9,9 @@ from django.conf import settings
from django.db import models, transaction from django.db import models, transaction
from mayan.apps.documents.literals import DOCUMENT_IMAGE_TASK_TIMEOUT from mayan.apps.documents.literals import DOCUMENT_IMAGE_TASK_TIMEOUT
from mayan.apps.documents.tasks import task_generate_document_page_image from mayan.apps.documents.tasks import (
task_generate_document_version_page_image
)
from .events import ( from .events import (
event_ocr_document_content_deleted, event_ocr_document_version_finish event_ocr_document_content_deleted, event_ocr_document_version_finish
@@ -20,47 +22,53 @@ from .signals import post_document_version_ocr
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DocumentPageOCRContentManager(models.Manager): class DocumentVesionPageOCRContentManager(models.Manager):
def delete_content_for(self, document, user=None): def delete_content_for(self, document, user=None):
with transaction.atomic(): with transaction.atomic():
for document_page in document.pages.all(): for document_page in document.pages.all():
self.filter(document_page=document_page).delete() self.filter(
document_version_page=document_page.content_object
).delete()
event_ocr_document_content_deleted.commit( event_ocr_document_content_deleted.commit(
actor=user, target=document actor=user, target=document
) )
def process_document_page(self, document_page): def process_document_version_page(self, document_version_page):
logger.info( logger.info(
'Processing page: %d of document version: %s', 'Processing page: %d of document version: %s',
document_page.page_number, document_page.document_version document_version_page.page_number,
document_version_page.document_version
) )
DocumentPageOCRContent = apps.get_model( DocumentVersionPageOCRContent = apps.get_model(
app_label='ocr', model_name='DocumentPageOCRContent' app_label='ocr', model_name='DocumentVersionPageOCRContent'
) )
task = task_generate_document_page_image.apply_async( task = task_generate_document_version_page_image.apply_async(
kwargs=dict( kwargs=dict(
document_page_id=document_page.pk document_version_page_id=document_version_page.pk
) )
) )
cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT, disable_sync_subtasks=False) cache_filename = task.get(
timeout=DOCUMENT_IMAGE_TASK_TIMEOUT, disable_sync_subtasks=False
with document_page.cache_partition.get_file(filename=cache_filename).open() as file_object:
document_page_content, created = DocumentPageOCRContent.objects.get_or_create(
document_page=document_page
) )
document_page_content.content = ocr_backend.execute(
with document_version_page.cache_partition.get_file(filename=cache_filename).open() as file_object:
document_version_page_content, created = DocumentVersionPageOCRContent.objects.get_or_create(
document_version_page=document_version_page
)
document_version_page_content.content = ocr_backend.execute(
file_object=file_object, file_object=file_object,
language=document_page.document.language language=document_version_page.document.language
) )
document_page_content.save() document_version_page_content.save()
logger.info( logger.info(
'Finished processing page: %d of document version: %s', 'Finished processing page: %d of document version: %s',
document_page.page_number, document_page.document_version document_version_page.page_number,
document_version_page.document_version
) )
def process_document_version(self, document_version): def process_document_version(self, document_version):
@@ -68,8 +76,10 @@ class DocumentPageOCRContentManager(models.Manager):
logger.debug('document version: %d', document_version.pk) logger.debug('document version: %d', document_version.pk)
try: try:
for document_page in document_version.pages.all(): for document_version_page in document_version.pages.all():
self.process_document_page(document_page=document_page) self.process_document_version_page(
document_version_page=document_version_page
)
except Exception as exception: except Exception as exception:
logger.error( logger.error(
'OCR error for document version: %d; %s', document_version.pk, 'OCR error for document version: %d; %s', document_version.pk,

View File

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
from datetime import timedelta from datetime import timedelta
from django.apps import apps
from django.utils.timezone import now from django.utils.timezone import now
from mayan.apps.common.settings import settings_db_sync_task_delay from mayan.apps.common.settings import settings_db_sync_task_delay
@@ -17,6 +18,17 @@ def method_document_ocr_submit(self):
latest_version.submit_for_ocr() latest_version.submit_for_ocr()
def method_document_page_get_ocr_content(self):
DocumentVersionPageOCRContent = apps.get_model(
app_label='ocr', model_name='DocumentVersionPageOCRContent'
)
try:
return self.content_object.ocr_content.content
except (AttributeError, DocumentVersionPageOCRContent.DoesNotExist):
return None
def method_document_version_ocr_submit(self): def method_document_version_ocr_submit(self):
event_ocr_document_version_submit.commit( event_ocr_document_version_submit.commit(
action_object=self.document, target=self action_object=self.document, target=self

View File

@@ -9,6 +9,10 @@ class Migration(migrations.Migration):
('documents', '__first__'), ('documents', '__first__'),
] ]
run_before = [
('documents', '0052_rename_document_page'),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='DocumentVersionOCRError', name='DocumentVersionOCRError',

View File

@@ -8,6 +8,9 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('ocr', '0007_auto_20170827_1617'), ('ocr', '0007_auto_20170827_1617'),
] ]
run_before = [
('documents', '0052_rename_document_page'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(

View File

@@ -0,0 +1,39 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('ocr', '0008_auto_20180917_0646'),
('documents', '0052_rename_document_page'),
]
operations = [
migrations.RenameModel(
'DocumentPageOCRContent', 'DocumentVersionPageOCRContent'
),
migrations.AlterField(
model_name='documentversionpageocrcontent',
name='document_page',
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name='ocr_content',
to='documents.DocumentVersionPage',
verbose_name='Document version page'
),
),
migrations.RenameField(
model_name='documentversionpageocrcontent',
old_name='document_page',
new_name='document_version_page',
),
migrations.AlterModelOptions(
name='documentversionpageocrcontent',
options={
'verbose_name': 'Document version page OCR content',
'verbose_name_plural': 'Document version pages OCR contents'
},
),
]

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