Compare commits

..

89 Commits

Author SHA1 Message Date
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
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
79 changed files with 1783 additions and 454 deletions

View File

@@ -63,6 +63,7 @@ job_docker_nightly:
only:
- nightly
- staging
- /^clients\/.+$/
job_documentation_build:
stage: build_documentation
@@ -152,7 +153,7 @@ job_push_python:
- locale-gen en_US.UTF-8
- update-locale LANG=en_US.UTF-8
- export LC_ALL=en_US.UTF-8
- apt-get install -qq curl exiftool gcc ghostscript gnupg1 graphviz libfuse2 libjpeg-dev libmagic1 libpng-dev libtiff-dev poppler-utils libreoffice poppler-utils python-dev python-virtualenv python3-dev tesseract-ocr tesseract-ocr-deu
- apt-get install -qq curl exiftool gcc ghostscript gnupg1 graphviz libfuse2 libjpeg-dev libmagic1 libpng-dev libtiff-dev poppler-utils libreoffice poppler-utils python-dev python3-dev python-virtualenv tesseract-ocr tesseract-ocr-deu
- virtualenv venv -p /usr/bin/python3
- . venv/bin/activate
- pip install -r requirements.txt -r requirements/testing-base.txt
@@ -162,6 +163,7 @@ job_push_python:
- releases/python
- staging
- nightly
- /^clients\/.+$/
test-mysql:
<<: *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

@@ -78,11 +78,6 @@
Support Docker networks and make it the default.
Delete the containers to allow the script to be idempotent.
Deploy a Redis container.
- Improve document version upload form.
- Use dropzone for document version upload form.
- Allow the "Execute document tools" permission to be
granted via ACL.
3.2.8 (2019-10-01)
==================

View File

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

View File

@@ -12,7 +12,8 @@ Changes
- Fix help text of the platformtemplate command.
- Fix IMAP4 mailbox.store flags argument. Python's documentation
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.
- Support configurable GUnicorn timeouts. Defaults to
current value of 120 seconds.

View File

@@ -93,7 +93,7 @@ Changes
Removals
--------
- Database conversion. Reason for removal: The database conversions support
- Database conversion. Reason for removal. The database conversions support
provided by this feature (SQLite to PostgreSQL) was being confused with
database migrations and upgrades.
@@ -108,7 +108,7 @@ Removals
Continued confusion about the purpose of the feature and confusion about
how errors with this feature were a reflexion of the code quality of
Mayan necessitated the removal of the database conversion feature.
Mayannecessitated the removal of the database conversion feature.
- Django environ

View File

@@ -2,8 +2,8 @@ from __future__ import unicode_literals
__title__ = 'Mayan EDMS'
__version__ = '3.3beta1'
__build__ = 0x030300
__build_string__ = 'v3.3beta1-9-g1b327b99f0_Tue Oct 8 15:15:08 2019 -0400'
__build__ = 0x030208
__build_string__ = 'v3.2.8-255-g69086d87dd_Tue Oct 8 09:43:10 2019 -0400'
__django_version__ = '1.11'
__author__ = 'Roberto Rosario'
__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 (
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 .events import (
event_user_authentication_error, event_user_password_reset_complete,
event_user_password_reset_started
)
from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
from .settings import setting_login_method, setting_maximum_session_length
@@ -57,6 +62,10 @@ class MayanLoginView(StrongholdPublicMixin, LoginView):
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):
if setting_login_method.value == 'email':
return EmailAuthenticationForm
@@ -112,6 +121,10 @@ class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmV
)
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):
extra_context = {
@@ -137,6 +150,10 @@ class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
)
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):
form_class = SetPasswordForm

View File

@@ -33,7 +33,7 @@ class CabinetDocumentUploadTestCase(CabinetTestMixin, GenericDocumentViewTestCas
def _request_upload_interactive_document_create_view(self):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
return self.post(
viewname='sources:document_upload_interactive', kwargs={
viewname='sources:upload_interactive', kwargs={
'source_id': self.test_source.pk
}, data={
'document_type_id': self.test_document_type.pk,

View File

@@ -376,7 +376,7 @@ class NewVersionBlockViewTestCase(
self.login_superuser()
response = self.post(
viewname='sources:document_version_upload', kwargs={
viewname='sources:upload_version', kwargs={
'document_pk': self.test_document.pk
}, follow=True
)

View File

@@ -53,8 +53,8 @@ class MultiFormView(DjangoFormView):
template_name = 'appearance/generic_form.html'
def _create_form(self, form_name, klass):
form_kwargs = self.get_form_kwargs(form_name=form_name)
form_create_method = 'create_{}_form'.format(form_name)
form_kwargs = self.get_form_kwargs(form_name)
form_create_method = 'create_%s_form' % form_name
if hasattr(self, form_create_method):
form = getattr(self, form_create_method)(**form_kwargs)
else:
@@ -66,17 +66,17 @@ class MultiFormView(DjangoFormView):
def dispatch(self, request, *args, **kwargs):
form_classes = self.get_form_classes()
self.forms = self.get_forms(form_classes=form_classes)
self.forms = self.get_forms(form_classes)
return super(MultiFormView, self).dispatch(request, *args, **kwargs)
def forms_valid(self, forms):
for form_name, form in forms.items():
form_valid_method = '{}_form_valid'.format(form_name)
form_valid_method = '%s_form_valid' % form_name
if hasattr(self, form_valid_method):
return getattr(self, form_valid_method)(form=form)
return getattr(self, form_valid_method)(form)
self.all_forms_valid(forms=forms)
self.all_forms_valid(forms)
return HttpResponseRedirect(redirect_to=self.get_success_url())
@@ -98,16 +98,14 @@ class MultiFormView(DjangoFormView):
def get_form_kwargs(self, form_name):
kwargs = {}
kwargs.update({'initial': self.get_initial(form_name=form_name)})
kwargs.update({'prefix': self.get_prefix(form_name=form_name)})
kwargs.update({'initial': self.get_initial(form_name)})
kwargs.update({'prefix': self.get_prefix(form_name)})
if self.request.method in ('POST', 'PUT'):
kwargs.update(
{
'data': self.request.POST,
'files': self.request.FILES,
}
)
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
})
kwargs.update(self.get_form_extra_kwargs(form_name=form_name) or {})
@@ -120,13 +118,13 @@ class MultiFormView(DjangoFormView):
return dict(
[
(
key, self._create_form(form_name=key, klass=klass)
key, self._create_form(key, klass)
) for key, klass in form_classes.items()
]
)
def get_initial(self, form_name):
initial_method = 'get_{}_initial'.format(form_name)
initial_method = 'get_%s_initial' % form_name
if hasattr(self, initial_method):
return getattr(self, initial_method)()
else:
@@ -208,9 +206,9 @@ class AddRemoveView(
getattr(self.main_object, self.related_field).add(*queryset)
else:
raise ImproperlyConfigured(
'View {} must be called with a main_object_method_add, a '
'View %s must be called with a main_object_method_add, a '
'related_field, or an action_add '
'method.'.format(self.__class__.__name__)
'method.' % self.__class__.__name__
)
def _action_remove(self, queryset):
@@ -227,9 +225,9 @@ class AddRemoveView(
getattr(self.main_object, self.related_field).remove(*queryset)
else:
raise ImproperlyConfigured(
'View {} must be called with a main_object_method_remove, a '
'View %s must be called with a main_object_method_remove, a '
'related_field, or an action_remove '
'method.'.format(self.__class__.__name__)
'method.' % self.__class__.__name__
)
def dispatch(self, request, *args, **kwargs):
@@ -350,10 +348,8 @@ class AddRemoveView(
def get_list_added_queryset(self):
if not self.related_field:
raise ImproperlyConfigured(
'View {} must be called with either a related_field or '
'override .get_list_added_queryset().'.format(
self.__class__.__name__
)
'View %s must be called with either a related_field or '
'override .get_list_added_queryset().' % self.__class__.__name__
)
return self.get_secondary_object_list().filter(

View File

@@ -199,3 +199,36 @@ class IndexToolsViewTestCase(
# An instance root exists
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

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

View File

@@ -162,6 +162,7 @@ link_workflow_template_transition_field_delete = Link(
tags='dangerous', text=_('Delete'),
view='document_states:workflow_template_transition_field_delete',
)
link_workflow_template_transition_field_edit = Link(
args='resolved_object.pk',
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 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.conf import settings
@@ -328,8 +333,8 @@ class WorkflowState(models.Model):
def save(self, *args, **kwargs):
# Solve issue #557 "Break workflows with invalid input"
# without using a migration.
# Remove blank=True, remove this, and create a migration in the next
# minor version.
# TODO: Remove blank=True, remove this, and create a migration in the
# next minor version.
try:
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.documents.tests.base import GenericDocumentViewTestCase
from ..literals import FIELD_TYPE_CHOICE_CHAR
from ..models import WorkflowTransition
from ..permissions import (
permission_workflow_edit, permission_workflow_view,
@@ -19,6 +20,11 @@ from .mixins import (
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(
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin,

View File

@@ -87,10 +87,10 @@ from .permissions import (
permission_document_download, permission_document_edit,
permission_document_new_version, permission_document_print,
permission_document_properties_edit, permission_document_restore,
permission_document_tools, permission_document_trash,
permission_document_type_delete, permission_document_type_edit,
permission_document_type_view, permission_document_version_revert,
permission_document_version_view, permission_document_view
permission_document_trash, permission_document_type_delete,
permission_document_type_edit, permission_document_type_view,
permission_document_version_revert, permission_document_version_view,
permission_document_view
)
# Just import to initialize the search models
from .search import document_search, document_page_search # NOQA
@@ -191,8 +191,8 @@ class DocumentsApp(MayanAppConfig):
permission_document_delete, permission_document_download,
permission_document_edit, permission_document_new_version,
permission_document_print, permission_document_properties_edit,
permission_document_restore, permission_document_tools,
permission_document_trash, permission_document_version_revert,
permission_document_restore, permission_document_trash,
permission_document_version_revert,
permission_document_version_view, permission_document_view,
permission_events_view, permission_transformation_create,
permission_transformation_delete,

View File

@@ -35,6 +35,7 @@ DOCUMENT_IMAGES_CACHE_NAME = 'document_images'
DOCUMENT_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.documents.storages.storage_documentimagecache'
STUB_EXPIRATION_INTERVAL = 60 * 60 * 24 # 24 hours
UPDATE_PAGE_COUNT_RETRY_DELAY = 10
UPLOAD_NEW_DOCUMENT_RETRY_DELAY = 10
UPLOAD_NEW_VERSION_RETRY_DELAY = 10
PAGE_RANGE_ALL = 'all'

View File

@@ -102,14 +102,6 @@ class Document(models.Model):
)
return RecentDocument.objects.add_document_for_user(user, self)
@property
def checksum(self):
return self.latest_version.checksum
@property
def date_updated(self):
return self.latest_version.timestamp
def delete(self, *args, **kwargs):
to_trash = kwargs.pop('to_trash', True)
@@ -134,14 +126,6 @@ class Document(models.Model):
else:
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):
return reverse(
viewname='documents:document_preview', kwargs={'pk': self.pk}
@@ -156,10 +140,6 @@ class Document(models.Model):
def is_in_trash(self):
return self.in_trash
@property
def latest_version(self):
return self.versions.order_by('timestamp').last()
def natural_key(self):
return (self.uuid,)
natural_key.dependencies = ['documents.DocumentType']
@@ -185,34 +165,6 @@ class Document(models.Model):
"""
return self.latest_version.open(*args, **kwargs)
@property
def page_count(self):
return self.latest_version.page_count
@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()
@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()
def restore(self):
self.in_trash = False
self.save()
@@ -257,3 +209,53 @@ class Document(models.Model):
@property
def size(self):
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

@@ -253,6 +253,10 @@ class DocumentVersion(models.Model):
)
return DocumentPage.passthrough.filter(document_version=self)
@property
def pages(self):
return self.version_pages.all()
@property
def page_count(self):
"""
@@ -260,10 +264,6 @@ class DocumentVersion(models.Model):
"""
return self.pages.count()
@property
def pages(self):
return self.version_pages.all()
def revert(self, _user=None):
"""
Delete the subsequent versions after this one

View File

@@ -78,3 +78,7 @@ queue_uploads.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_for',
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

@@ -9,7 +9,8 @@ from django.db import OperationalError
from mayan.celery import app
from .literals import (
UPDATE_PAGE_COUNT_RETRY_DELAY, UPLOAD_NEW_VERSION_RETRY_DELAY
UPDATE_PAGE_COUNT_RETRY_DELAY, UPLOAD_NEW_DOCUMENT_RETRY_DELAY,
UPLOAD_NEW_VERSION_RETRY_DELAY
)
logger = logging.getLogger(__name__)
@@ -121,6 +122,60 @@ def task_update_page_count(self, version_id):
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)
def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, comment=None):
SharedUploadedFile = apps.get_model(

View File

@@ -5,7 +5,6 @@ import os
from django.conf import settings
from mayan.apps.converter.classes import Layer
from mayan.apps.converter.layers import layer_saved_transformations
from ..literals import PAGE_RANGE_ALL
from ..models import DocumentType
@@ -15,7 +14,6 @@ from .literals import (
TEST_DOCUMENT_TYPE_LABEL, TEST_DOCUMENT_TYPE_LABEL_EDITED,
TEST_DOCUMENT_TYPE_QUICK_LABEL, TEST_DOCUMENT_TYPE_QUICK_LABEL_EDITED,
TEST_SMALL_DOCUMENT_FILENAME, TEST_SMALL_DOCUMENT_PATH,
TEST_TRANSFORMATION_ARGUMENT, TEST_TRANSFORMATION_CLASS,
TEST_VERSION_COMMENT
)
@@ -71,8 +69,6 @@ class DocumentTestMixin(object):
self.test_document = document
self.test_documents.append(document)
self.test_document_page = document.latest_version.pages.first()
self.test_document_version = document.latest_version
class DocumentTypeViewTestMixin(object):
@@ -153,13 +149,6 @@ class DocumentVersionTestMixin(object):
class DocumentViewTestMixin(object):
def _create_document_transformation(self):
layer_saved_transformations.add_transformation_to(
obj=self.test_document.pages.first(),
transformation_class=TEST_TRANSFORMATION_CLASS,
arguments=TEST_TRANSFORMATION_ARGUMENT
)
def _request_document_properties_view(self):
return self.get(
viewname='documents:document_properties',

View File

@@ -9,10 +9,10 @@ from ..permissions import (
from .base import GenericDocumentViewTestCase
class DocumentPageDisableViewTestMixin(object):
def _disable_test_document_page(self):
self.test_document_page.enabled = False
self.test_document_page.save()
class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase):
def setUp(self):
super(DocumentPageDisableViewTestCase, self).setUp()
self.test_document_page = self.test_document.pages_all.first()
def _request_test_document_page_disable_view(self):
return self.post(
@@ -21,31 +21,6 @@ class DocumentPageDisableViewTestMixin(object):
}
)
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):
test_document_page_count = self.test_document.pages.count()
@@ -70,6 +45,13 @@ class DocumentPageDisableViewTestCase(
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):
test_document_page_count = self.test_document.pages.count()
@@ -94,6 +76,17 @@ class DocumentPageDisableViewTestCase(
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):
self._disable_test_document_page()
@@ -121,6 +114,13 @@ class DocumentPageDisableViewTestCase(
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):
self._disable_test_document_page()
test_document_page_count = self.test_document.pages.count()
@@ -148,7 +148,7 @@ class DocumentPageDisableViewTestCase(
)
class DocumentPageViewTestMixin(object):
class DocumentPageViewTestCase(GenericDocumentViewTestCase):
def _request_test_document_page_list_view(self):
return self.get(
viewname='documents:document_pages', kwargs={
@@ -156,17 +156,6 @@ class DocumentPageViewTestMixin(object):
}
)
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):
response = self._request_test_document_page_list_view()
self.assertEqual(response.status_code, 404)
@@ -181,6 +170,13 @@ class DocumentPageViewTestCase(
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):
response = self._request_test_document_page_view(
document_page=self.test_document.pages.first()

View File

@@ -203,7 +203,7 @@ class DocumentTypeQuickLabelViewsTestCase(
)
class DocumentsQuickLabelViewTestMixin(object):
class DocumentsQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, GenericDocumentViewTestCase):
def _request_document_quick_label_edit_view(self, extra_data=None):
data = {
'document_type_available_filenames': self.test_document_type_filename.pk,
@@ -219,11 +219,6 @@ class DocumentsQuickLabelViewTestMixin(object):
}, data=data
)
class DocumentsQuickLabelViewTestCase(
DocumentsQuickLabelViewTestMixin, DocumentTypeQuickLabelTestMixin,
GenericDocumentViewTestCase
):
def test_document_quick_label_no_permission(self):
self._create_test_quick_label()

View File

@@ -9,24 +9,13 @@ from .literals import TEST_VERSION_COMMENT
from .mixins import DocumentVersionTestMixin
class DocumentVersionViewTestMixin(object):
class DocumentVersionTestCase(DocumentVersionTestMixin, GenericDocumentViewTestCase):
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}
)
class DocumentVersionViewTestCase(
DocumentVersionTestMixin, DocumentVersionViewTestMixin,
GenericDocumentViewTestCase
):
def test_document_version_list_no_permission(self):
self._upload_new_version()
@@ -44,6 +33,12 @@ class DocumentVersionViewTestCase(
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):
first_version = self.test_document.latest_version
self._upload_new_version()

View File

@@ -16,14 +16,22 @@ from ..permissions import (
from .base import GenericDocumentViewTestCase
from .literals import (
TEST_DOCUMENT_TYPE_2_LABEL, TEST_SMALL_DOCUMENT_FILENAME
TEST_DOCUMENT_TYPE_2_LABEL, TEST_SMALL_DOCUMENT_FILENAME,
TEST_TRANSFORMATION_ARGUMENT, TEST_TRANSFORMATION_CLASS
)
from .mixins import DocumentViewTestMixin
class DocumentViewTestCase(
class DocumentsViewsTestCase(
LayerTestMixin, DocumentViewTestMixin, GenericDocumentViewTestCase
):
def _create_document_transformation(self):
layer_saved_transformations.add_transformation_to(
obj=self.test_document.pages.first(),
transformation_class=TEST_TRANSFORMATION_CLASS,
arguments=TEST_TRANSFORMATION_ARGUMENT
)
def test_document_view_no_permissions(self):
response = self._request_document_properties_view()
self.assertEqual(response.status_code, 404)
@@ -294,13 +302,13 @@ class DocumentViewTestCase(
self.assertEqual(self.test_document.pages.count(), 0)
def test_document_update_page_count_view_with_permission(self):
# TODO: Revise permission association
page_count = self.test_document.pages.count()
self.test_document.pages.all().delete()
self.assertEqual(self.test_document.pages.count(), 0)
self.grant_access(
obj=self.test_document, permission=permission_document_tools
)
self.grant_permission(permission=permission_document_tools)
response = self._request_document_update_page_count_view()
self.assertEqual(response.status_code, 302)
@@ -321,9 +329,7 @@ class DocumentViewTestCase(
self.test_document.pages.all().delete()
self.assertEqual(self.test_document.pages.count(), 0)
self.grant_access(
obj=self.test_document, permission=permission_document_tools
)
self.grant_permission(permission=permission_document_tools)
response = self._request_document_multiple_update_page_count_view()
self.assertEqual(response.status_code, 302)

View File

@@ -5,7 +5,7 @@ from ..permissions import permission_document_view
from .base import GenericDocumentViewTestCase
class DuplicatedDocumentsViewsTestMixin(object):
class DuplicatedDocumentsViewsTestCase(GenericDocumentViewTestCase):
def _upload_duplicate_document(self):
self.upload_document()
@@ -18,10 +18,6 @@ class DuplicatedDocumentsViewsTestMixin(object):
kwargs={'pk': self.test_documents[0].pk}
)
class DuplicatedDocumentsViewsTestCase(
DuplicatedDocumentsViewsTestMixin, GenericDocumentViewTestCase
):
def test_duplicated_document_list_no_permissions(self):
self._upload_duplicate_document()

View File

@@ -17,23 +17,12 @@ TEST_TRANSFORMATION_NAME = 'rotate'
TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180'
class DocumentEventsTestMixin(object):
class DocumentEventsTestCase(GenericDocumentViewTestCase):
def _request_test_document_download_view(self):
return self.get(
'documents:document_download', kwargs={'pk': self.test_document.pk}
)
def _request_test_document_preview_view(self):
return self.get(
viewname='documents:document_preview', kwargs={
'pk': self.test_document.pk
}
)
class DocumentEventsTestCase(
DocumentEventsTestMixin, GenericDocumentViewTestCase
):
def test_document_download_event_no_permissions(self):
Action.objects.all().delete()
@@ -66,6 +55,13 @@ class DocumentEventsTestCase(
self.assertEqual(event.target, self.test_document)
self.assertEqual(event.verb, event_document_download.id)
def _request_test_document_preview_view(self):
return self.get(
viewname='documents:document_preview', kwargs={
'pk': self.test_document.pk
}
)
def test_document_view_event_no_permissions(self):
Action.objects.all().delete()

View File

@@ -6,34 +6,13 @@ from ..permissions import permission_document_view
from .base import GenericDocumentViewTestCase
class FavoriteDocumentsTestMixin(object):
class FavoriteDocumentsTestCase(GenericDocumentViewTestCase):
def _request_document_add_to_favorites_view(self):
return self.post(
viewname='documents:document_add_to_favorites',
kwargs={'pk': self.test_document.pk}
)
def _document_add_to_favorites(self):
FavoriteDocument.objects.add_for_user(
document=self.test_document, user=self._test_case_user
)
def _request_document_list_favorites(self):
return self.get(
viewname='documents:document_list_favorites',
)
def _request_document_remove_from_favorites(self):
return self.post(
viewname='documents:document_remove_from_favorites',
kwargs={'pk': self.test_document.pk}
)
class FavoriteDocumentsTestCase(
FavoriteDocumentsTestMixin, GenericDocumentViewTestCase
):
def test_document_add_to_favorites_view_no_permission(self):
response = self._request_document_add_to_favorites_view()
self.assertEqual(response.status_code, 404)
@@ -47,6 +26,16 @@ class FavoriteDocumentsTestCase(
self.assertEqual(response.status_code, 302)
self.assertEqual(FavoriteDocument.objects.count(), 1)
def _document_add_to_favorites(self):
FavoriteDocument.objects.add_for_user(
document=self.test_document, user=self._test_case_user
)
def _request_document_list_favorites(self):
return self.get(
viewname='documents:document_list_favorites',
)
def test_document_list_favorites_view_no_permission(self):
self._document_add_to_favorites()
response = self._request_document_list_favorites()
@@ -64,6 +53,12 @@ class FavoriteDocumentsTestCase(
response=response, text=self.test_document.label, status_code=200
)
def _request_document_remove_from_favorites(self):
return self.post(
viewname='documents:document_remove_from_favorites',
kwargs={'pk': self.test_document.pk}
)
def test_document_remove_from_favorites_view_no_permission(self):
self._document_add_to_favorites()
response = self._request_document_remove_from_favorites()

View File

@@ -89,6 +89,7 @@ class DocumentsLinksTestCase(GenericDocumentViewTestCase):
class DeletedDocumentsLinksTestCase(GenericDocumentViewTestCase):
def setUp(self):
super(DeletedDocumentsLinksTestCase, self).setUp()
self.login_user()
self.test_document.delete()
self.test_deleted_document = DeletedDocument.objects.get(
pk=self.test_document.pk

View File

@@ -1,12 +1,12 @@
from __future__ import unicode_literals
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 .base import GenericDocumentViewTestCase
from mayan.apps.documents.tests.mixins import DocumentTestMixin
class DocumentSearchTestMixin(object):
class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase):
def _perform_document_page_search(self):
return document_page_search.search(
query_string={'q': self.test_document.label}, user=self._test_case_user
@@ -17,10 +17,6 @@ class DocumentSearchTestMixin(object):
query_string={'q': self.test_document.label}, user=self._test_case_user
)
class DocumentSearchTestCase(
DocumentSearchTestMixin, GenericDocumentViewTestCase
):
def test_document_page_search_no_access(self):
queryset = self._perform_document_page_search()
self.assertFalse(self.test_document.pages.first() in queryset)

View File

@@ -9,7 +9,7 @@ from ..permissions import (
from .base import GenericDocumentViewTestCase
class TrashedDocumentTestMixin(object):
class TrashedDocumentTestCase(GenericDocumentViewTestCase):
def _request_document_restore_get_view(self):
return self.get(
viewname='documents:document_restore', kwargs={
@@ -17,49 +17,6 @@ class TrashedDocumentTestMixin(object):
}
)
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_document_delete_get_view(self):
return self.get(
viewname='documents:document_delete', kwargs={
'pk': self.test_document.pk
}
)
def _request_document_delete_post_view(self):
return self.post(
viewname='documents:document_delete', kwargs={
'pk': self.test_document.pk
}
)
def _request_document_list_deleted_view(self):
return self.get(viewname='documents:document_list_deleted')
class TrashedDocumentTestCase(
TrashedDocumentTestMixin, GenericDocumentViewTestCase
):
def test_document_restore_get_view_no_permission(self):
self.test_document.delete()
self.assertEqual(Document.objects.count(), 0)
@@ -86,6 +43,13 @@ class TrashedDocumentTestCase(
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):
self.test_document.delete()
self.assertEqual(Document.objects.count(), 0)
@@ -110,6 +74,13 @@ class TrashedDocumentTestCase(
self.assertEqual(DeletedDocument.objects.count(), 0)
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):
document_count = Document.objects.count()
@@ -130,6 +101,13 @@ class TrashedDocumentTestCase(
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):
response = self._request_document_trash_post_view()
self.assertEqual(response.status_code, 404)
@@ -148,6 +126,13 @@ class TrashedDocumentTestCase(
self.assertEqual(DeletedDocument.objects.count(), 1)
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):
self.test_document.delete()
self.assertEqual(Document.objects.count(), 0)
@@ -180,6 +165,13 @@ class TrashedDocumentTestCase(
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):
self.test_document.delete()
self.assertEqual(Document.objects.count(), 0)
@@ -206,6 +198,9 @@ class TrashedDocumentTestCase(
self.assertEqual(DeletedDocument.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):
self.test_document.delete()

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals
from django.apps import apps
from django.db.models.signals import post_migrate
from django.utils.translation import ugettext_lazy as _
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 .dependencies import * # NOQA
from .handlers import handler_create_system_user
from .html_widgets import (
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
), 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,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

@@ -34,7 +34,7 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT
def test_upload_interactive_with_unicode_metadata(self):
url = URL(
path=reverse(viewname='sources:document_upload_interactive')
path=reverse(viewname='sources:upload_interactive')
)
url.args['metadata0_id'] = self.test_metadata_type.pk
url.args['metadata0_value'] = TEST_METADATA_VALUE_UNICODE
@@ -61,7 +61,7 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT
def test_upload_interactive_with_ampersand_metadata(self):
url = URL(
path=reverse(viewname='sources:document_upload_interactive')
path=reverse(viewname='sources:upload_interactive')
)
url.args['metadata0_id'] = self.test_metadata_type.pk
url.args['metadata0_value'] = TEST_METADATA_VALUE_WITH_AMPERSAND

View File

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

View File

@@ -108,12 +108,6 @@ def navigation_resolve_menus(context, names, source=None, sort_results=None):
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)
def navigation_source_column_resolve(context, column):
if column:
@@ -121,3 +115,9 @@ def navigation_source_column_resolve(context, column):
return result
else:
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

@@ -0,0 +1,11 @@
from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_redaction_create = Icon(
driver_name='fontawesome-dual', primary_symbol='highlighter',
secondary_symbol='plus'
)
icon_redaction_delete = Icon(driver_name='fontawesome', symbol='times')
icon_redaction_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_redactions = Icon(driver_name='fontawesome', symbol='highlighter')

View File

@@ -145,9 +145,8 @@ class SourcesApp(MayanAppConfig):
menu_secondary.bind_links(
links=(link_document_version_upload,),
sources=(
'documents:document_version_list',
'documents:document_version_revert',
'sources:document_version_upload'
'documents:document_version_list', 'documents:upload_version',
'documents:document_version_revert'
)
)

View File

@@ -23,11 +23,14 @@ class NewDocumentForm(DocumentForm):
class NewVersionForm(forms.Form):
comment = forms.CharField(
help_text=_('An optional comment to explain the upload.'),
label=_('Comment'), required=False,
widget=forms.widgets.Textarea(attrs={'rows': 4}),
)
def __init__(self, *args, **kwargs):
super(NewVersionForm, self).__init__(*args, **kwargs)
self.fields['comment'] = forms.CharField(
label=_('Comment'),
required=False,
widget=forms.widgets.Textarea(attrs={'rows': 4}),
)
class UploadBaseForm(forms.Form):

View File

@@ -117,7 +117,7 @@ link_document_version_upload = Link(
args='resolved_object.pk', condition=document_new_version_not_blocked,
icon_class_path='mayan.apps.sources.icons.icon_document_version_upload',
permissions=(permission_document_new_version,),
text=_('Upload new version'), view='sources:document_version_upload',
text=_('Upload new version'), view='sources:upload_version',
)
link_setup_source_logs = Link(
args=('resolved_object.pk',),

View File

@@ -1,115 +0,0 @@
{% load i18n %}
{% load static %}
<link href="{% static 'sources/node_modules/dropzone/dist/dropzone.css' %}" media="screen" rel="stylesheet" type="text/css" />
<style>
.dropzone .dz-preview .dz-details {
top: 25px;
}
.dropzone .dz-preview .dz-progress {
top: 60%;
}
.dropzone .dz-preview .dz-error-message {
top: 130px;
}
.dropzone-previews, .dropzone .dz-message {
border: 2px solid rgba(0, 0, 0, 0.3);
padding: 40px 20px;
}
.dropzone .dz-preview .dz-error-message {
overflow-wrap: break-word;
padding: 1.2em;
top: 180px;
}
.dropzone .dz-preview .dz-details .dz-filename span,
.dropzone .dz-preview .dz-details .dz-size span {
padding: 0px;
}
.dropzone {
min-height: 150px;
border: inherit;
background: inherit;
padding: 0px;
}
.dropzone-previews, .dropzone .dz-message {
background: white;
}
}
</style>
<script type="text/x-template" id="previewTemplate">
<div class="dz-preview dz-file-preview">
<i class="far fa-file fa-10x"></i>
<div class="dz-details">
<div class="dz-filename"><span data-dz-name></span></div>
<div class="dz-size" data-dz-size></div>
<img data-dz-thumbnail />
</div>
<div class="dz-progress">
<span class="dz-upload" data-dz-uploadprogress>
<div class="progress-bar progress-bar-success progress-bar-striped active" role="progressbar" style="width: 100%">
</div>
</span>
</div>
<div class="dz-success-mark">
<span>
<i class="text-success fa fa-4x fa-check-circle"></i>
</span>
</div>
<div class="dz-error-mark">
<span>
<i class="text-danger fa fa-4x fa-times-circle"></i>
</span>
</div>
<div class="dz-error-message">
<span data-dz-errormessage>
</span>
</div>
</div>
</script>
<script>
var messageCancelUpload = "{% trans 'Cancel upload' %}";
var messageCancelUploadConfirmation = "{% trans 'Are you sure you want to cancel this upload?' %}";
var messageDefaultMessage = "{% trans 'Drop files or click here to upload files' %}";
var messageFallbackMessage = "{% trans 'Your browser does not support drag and drop file uploads.' %}";
var messageFallbackText = "{% trans 'Please use the fallback form below to upload your files.' %}";
var messageRemoveFile = "{% trans 'Clear' %}";
var messageResponseError = "{% trans 'Server responded with {{statusCode}} code.' %}";
$.getScript( "{% static 'sources/node_modules/dropzone/dist/dropzone.js' %}" )
.done(function( script, textStatus ) {
Dropzone.autoDiscover = false;
jQuery(document).ready(function() {
var previewTemplate = document.querySelector('#previewTemplate').innerHTML;
{% verbatim %}
$('.dropzone').dropzone({
addRemoveLinks: true,
createImageThumbnails: false,
dictCancelUpload: messageCancelUpload,
dictCancelUploadConfirmation: messageCancelUploadConfirmation,
dictDefaultMessage: '<i class="fa fa-cloud-upload-alt"></i> ' + messageDefaultMessage,
dictFallbackMessage: messageFallbackMessage,
dictFallbackText: messageFallbackText,
dictRemoveFile: messageRemoveFile,
dictResponseError: messageResponseError,
maxFilesize: 2048,
paramName: 'source-file',
previewTemplate: previewTemplate,
timeout: 1200000
});
{% endverbatim %}
});
});
</script>

View File

@@ -1,2 +1,118 @@
{% load i18n %}
{% load static %}
<link href="{% static 'sources/node_modules/dropzone/dist/dropzone.css' %}" media="screen" rel="stylesheet" type="text/css" />
<style>
.dropzone .dz-preview .dz-details {
top: 25px;
}
.dropzone .dz-preview .dz-progress {
top: 60%;
}
.dropzone .dz-preview .dz-error-message {
top: 130px;
}
.dropzone-previews, .dropzone .dz-message {
border: 2px solid rgba(0, 0, 0, 0.3);
padding: 40px 20px;
}
.dropzone .dz-preview .dz-error-message {
overflow-wrap: break-word;
padding: 1.2em;
top: 180px;
}
.dropzone .dz-preview .dz-details .dz-filename span,
.dropzone .dz-preview .dz-details .dz-size span {
padding: 0px;
}
.dropzone {
min-height: 150px;
border: inherit;
background: inherit;
padding: 0px;
}
.dropzone-previews, .dropzone .dz-message {
background: white;
}
}
</style>
{% include 'appearance/generic_multiform_subtemplate.html' %}
{% include 'sources/dropzone.html' %}
<script type="text/x-template" id="previewTemplate">
<div class="dz-preview dz-file-preview">
<i class="far fa-file fa-10x"></i>
<div class="dz-details">
<div class="dz-filename"><span data-dz-name></span></div>
<div class="dz-size" data-dz-size></div>
<img data-dz-thumbnail />
</div>
<div class="dz-progress">
<span class="dz-upload" data-dz-uploadprogress>
<div class="progress-bar progress-bar-success progress-bar-striped active" role="progressbar" style="width: 100%">
</div>
</span>
</div>
<div class="dz-success-mark">
<span>
<i class="text-success fa fa-4x fa-check-circle"></i>
</span>
</div>
<div class="dz-error-mark">
<span>
<i class="text-danger fa fa-4x fa-times-circle"></i>
</span>
</div>
<div class="dz-error-message">
<span data-dz-errormessage>
</span>
</div>
</div>
</script>
<script>
var messageCancelUpload = "{% trans 'Cancel upload' %}";
var messageCancelUploadConfirmation = "{% trans 'Are you sure you want to cancel this upload?' %}";
var messageDefaultMessage = "{% trans 'Drop files or click here to upload files' %}";
var messageFallbackMessage = "{% trans 'Your browser does not support drag and drop file uploads.' %}";
var messageFallbackText = "{% trans 'Please use the fallback form below to upload your files.' %}";
var messageRemoveFile = "{% trans 'Clear' %}";
var messageResponseError = "{% trans 'Server responded with {{statusCode}} code.' %}";
$.getScript( "{% static 'sources/node_modules/dropzone/dist/dropzone.js' %}" )
.done(function( script, textStatus ) {
Dropzone.autoDiscover = false;
jQuery(document).ready(function() {
var previewTemplate = document.querySelector('#previewTemplate').innerHTML;
{% verbatim %}
$('.dropzone').dropzone({
addRemoveLinks: true,
createImageThumbnails: false,
dictCancelUpload: messageCancelUpload,
dictCancelUploadConfirmation: messageCancelUploadConfirmation,
dictDefaultMessage: '<i class="fa fa-cloud-upload-alt"></i> ' + messageDefaultMessage,
dictFallbackMessage: messageFallbackMessage,
dictFallbackText: messageFallbackText,
dictRemoveFile: messageRemoveFile,
dictResponseError: messageResponseError,
maxFilesize: 2048,
paramName: 'source-file',
previewTemplate: previewTemplate,
timeout: 1200000
});
{% endverbatim %}
});
});
</script>

View File

@@ -7,13 +7,6 @@ from .literals import TEST_SOURCE_LABEL, TEST_SOURCE_UNCOMPRESS_N
class SourceTestMixin(object):
auto_create_test_source = True
def setUp(self):
super(SourceTestMixin, self).setUp()
if self.auto_create_test_source:
self._create_test_source()
def _create_test_source(self):
self.test_source = WebFormSource.objects.create(
enabled=True, label=TEST_SOURCE_LABEL,

View File

@@ -39,6 +39,7 @@ class CompressedUploadsTestCase(SourceTestMixin, GenericDocumentTestCase):
auto_upload_document = False
def test_upload_compressed_file(self):
self._create_test_source()
self.test_source.uncompress = SOURCE_UNCOMPRESS_CHOICE_Y
self.test_source.save()

View File

@@ -32,7 +32,7 @@ class DocumentUploadWizardViewTestMixin(object):
def _request_upload_wizard_view(self, document_path=TEST_SMALL_DOCUMENT_PATH):
with open(document_path, mode='rb') as file_object:
return self.post(
viewname='sources:document_upload_interactive', kwargs={
viewname='sources:upload_interactive', kwargs={
'source_id': self.test_source.pk
}, data={
'source-file': file_object,
@@ -42,7 +42,7 @@ class DocumentUploadWizardViewTestMixin(object):
def _request_upload_interactive_view(self):
return self.get(
viewname='sources:document_upload_interactive', data={
viewname='sources:upload_interactive', data={
'document_type_id': self.test_document_type.pk,
}
)
@@ -54,6 +54,10 @@ class DocumentUploadWizardViewTestCase(
):
auto_upload_document = False
def setUp(self):
super(DocumentUploadWizardViewTestCase, self).setUp()
self._create_test_source()
def test_upload_compressed_file(self):
self.test_source.uncompress = SOURCE_UNCOMPRESS_CHOICE_Y
self.test_source.save()
@@ -109,7 +113,7 @@ class DocumentUploadWizardViewTestCase(
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
response = self.post(
viewname='sources:document_upload_interactive', kwargs={
viewname='sources:upload_interactive', kwargs={
'source_id': self.test_source.pk
}, data={
'source-file': file_object,
@@ -153,7 +157,7 @@ class DocumentUploadIssueTestCase(GenericDocumentViewTestCase):
# Upload the test document
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.post(
viewname='sources:document_upload_interactive', data={
viewname='sources:upload_interactive', data={
'document-language': 'eng',
'source-file': file_object,
'document_type_id': self.test_document_type.pk
@@ -203,7 +207,7 @@ class NewDocumentVersionViewTestCase(GenericDocumentViewTestCase):
NewVersionBlock.objects.block(self.test_document)
response = self.post(
viewname='sources:document_version_upload', kwargs={
viewname='sources:upload_version', kwargs={
'document_pk': self.test_document.pk
}, follow=True
)
@@ -296,8 +300,6 @@ class StagingFolderViewTestCase(
class SourcesViewTestCase(
SourceTestMixin, SourceViewTestMixin, GenericViewTestCase
):
auto_create_test_source = False
def test_source_create_view_no_permission(self):
response = self._request_setup_source_create_view()
self.assertEqual(response.status_code, 403)

View File

@@ -9,7 +9,7 @@ from .api_views import (
from .views import (
SetupSourceCheckView, SetupSourceCreateView, SetupSourceDeleteView,
SetupSourceEditView, SetupSourceListView, SourceLogListView,
StagingFileDeleteView, DocumentVersionUploadInteractiveView, UploadInteractiveView
StagingFileDeleteView, UploadInteractiveVersionView, UploadInteractiveView
)
from .wizards import DocumentCreateWizard
@@ -27,23 +27,19 @@ urlpatterns = [
),
url(
regex=r'^documents/upload/new/interactive/(?P<source_id>\d+)/$',
view=UploadInteractiveView.as_view(),
name='document_upload_interactive'
view=UploadInteractiveView.as_view(), name='upload_interactive'
),
url(
regex=r'^documents/upload/new/interactive/$',
view=UploadInteractiveView.as_view(),
name='document_upload_interactive'
view=UploadInteractiveView.as_view(), name='upload_interactive'
),
url(
regex=r'^documents/(?P<document_pk>\d+)/versions/upload/interactive/(?P<source_id>\d+)/$',
view=DocumentVersionUploadInteractiveView.as_view(),
name='document_version_upload'
view=UploadInteractiveVersionView.as_view(), name='upload_version'
),
url(
regex=r'^documents/(?P<document_pk>\d+)/versions/upload/interactive/$',
view=DocumentVersionUploadInteractiveView.as_view(),
name='document_version_upload'
view=UploadInteractiveVersionView.as_view(), name='upload_version'
),
# Setup views

View File

@@ -84,10 +84,10 @@ class UploadBaseView(MultiFormView):
@staticmethod
def get_tab_link_for_source(source, document=None):
if document:
view = 'sources:document_version_upload'
view = 'sources:upload_version'
args = ('"{}"'.format(document.pk), '"{}"'.format(source.pk),)
else:
view = 'sources:document_upload_interactive'
view = 'sources:upload_interactive'
args = ('"{}"'.format(source.pk),)
return Link(
@@ -180,8 +180,8 @@ class UploadBaseView(MultiFormView):
},
})
menu_facet.bound_links['sources:document_upload_interactive'] = self.tab_links
menu_facet.bound_links['sources:document_version_upload'] = self.tab_links
menu_facet.bound_links['sources:upload_interactive'] = self.tab_links
menu_facet.bound_links['sources:upload_version'] = self.tab_links
context.update(
{
@@ -360,7 +360,7 @@ class UploadInteractiveView(UploadBaseView):
return context
class DocumentVersionUploadInteractiveView(UploadBaseView):
class UploadInteractiveVersionView(UploadBaseView):
def dispatch(self, request, *args, **kwargs):
self.subtemplates_list = []
@@ -392,7 +392,7 @@ class DocumentVersionUploadInteractiveView(UploadBaseView):
self.tab_links = UploadBaseView.get_active_tab_links(self.document)
return super(
DocumentVersionUploadInteractiveView, self
UploadInteractiveVersionView, self
).dispatch(request, *args, **kwargs)
def forms_valid(self, forms):
@@ -448,36 +448,27 @@ class DocumentVersionUploadInteractiveView(UploadBaseView):
files=kwargs.get('files', None),
)
def create_document_form_form(self, **kwargs):
return self.get_form_classes()['document_form'](
prefix=kwargs['prefix'],
data=kwargs.get('data', None),
files=kwargs.get('files', None),
)
def get_form_classes(self):
source_form_class = get_upload_form_class(self.source.source_type)
# Override source form class to enable the HTML5 file uploader
if source_form_class == WebFormUploadForm:
source_form_class = WebFormUploadFormHTML5
return {
'document_form': NewVersionForm,
'source_form': source_form_class
'source_form': get_upload_form_class(self.source.source_type)
}
def get_context_data(self, **kwargs):
context = super(
DocumentVersionUploadInteractiveView, self
UploadInteractiveVersionView, self
).get_context_data(**kwargs)
context['object'] = self.document
context['title'] = _(
'Upload a new version for document "%(document)s" '
'from source: %(source)s'
) % {'document': self.document, 'source': self.source.label}
context['submit_label'] = _('Submit')
context['form_css_classes'] = 'dropzone'
context['form_disable_submit'] = True
context['form_action'] = '{}?{}'.format(
reverse(
viewname=self.request.resolver_match.view_name,
kwargs=self.request.resolver_match.kwargs
), self.request.META['QUERY_STRING']
)
'Upload a new version from source: %s'
) % self.source.label
return context

View File

@@ -201,7 +201,7 @@ class DocumentCreateWizard(SessionWizardView):
for step in WizardStep.get_all():
query_dict.update(step.done(wizard=self) or {})
url = furl(reverse(viewname='sources:document_upload_interactive'))
url = furl(reverse(viewname='sources:upload_interactive'))
# Use equal and not .update() to get the same result as using
# urlencode(doseq=True)
url.args = query_dict

Binary file not shown.

View File

@@ -27,7 +27,7 @@ class TaggedDocumentUploadTestCase(TagTestMixin, GenericDocumentViewTestCase):
def _request_upload_interactive_document_create_view(self):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
return self.post(
viewname='sources:document_upload_interactive', kwargs={
viewname='sources:upload_interactive', kwargs={
'source_id': self.test_source.pk
}, data={
'document_type_id': self.test_document_type.pk,

View File

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

View File

@@ -0,0 +1,15 @@
from __future__ import unicode_literals
from django.contrib import admin
from .models import WebLink
@admin.register(WebLink)
class WebLinkAdmin(admin.ModelAdmin):
def document_type_list(self, instance):
return ','.join(
instance.document_types.values_list('label', flat=True)
)
filter_horizontal = ('document_types',)
list_display = ('label', 'template', 'enabled', 'document_type_list')

123
mayan/apps/weblinks/apps.py Normal file
View File

@@ -0,0 +1,123 @@
from __future__ import unicode_literals
from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.acls.links import link_acl_list
from mayan.apps.acls.permissions import permission_acl_edit, permission_acl_view
from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.html_widgets import TwoStateWidget
from mayan.apps.common.menus import (
menu_facet, menu_list_facet, menu_object, menu_secondary, menu_setup
)
from mayan.apps.events.classes import ModelEventType
from mayan.apps.events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list
)
from mayan.apps.navigation.classes import SourceColumn
from .events import event_web_link_edited
from .links import (
link_document_type_web_links, link_document_web_link_list,
link_web_link_create, link_web_link_delete, link_web_link_document_types,
link_web_link_edit, link_web_link_instance_view,
link_web_link_list, link_web_link_setup
)
from .permissions import (
permission_web_link_delete, permission_web_link_edit,
permission_web_link_instance_view, permission_web_link_view
)
class WebLinksApp(MayanAppConfig):
app_namespace = 'weblinks'
app_url = 'weblinks'
has_rest_api = False
has_tests = False
name = 'mayan.apps.weblinks'
verbose_name = _('Links')
def ready(self):
super(WebLinksApp, self).ready()
from actstream import registry
Document = apps.get_model(
app_label='documents', model_name='Document'
)
DocumentType = apps.get_model(
app_label='documents', model_name='DocumentType'
)
ResolvedWebLink = self.get_model(model_name='ResolvedWebLink')
WebLink = self.get_model(model_name='WebLink')
ModelEventType.register(
event_types=(
event_web_link_edited,
), model=WebLink
)
ModelPermission.register(
model=Document, permissions=(
permission_web_link_instance_view,
)
)
ModelPermission.register(
model=DocumentType, permissions=(
permission_web_link_instance_view,
)
)
ModelPermission.register(
model=WebLink, permissions=(
permission_acl_edit, permission_acl_view,
permission_web_link_delete, permission_web_link_edit,
permission_web_link_view
)
)
SourceColumn(
attribute='label', is_identifier=True, is_sortable=True,
source=ResolvedWebLink
)
SourceColumn(
attribute='label', is_identifier=True, is_sortable=True,
source=WebLink
)
SourceColumn(
attribute='enabled', is_sortable=True, source=WebLink,
widget=TwoStateWidget
)
menu_facet.bind_links(
links=(link_document_web_link_list,),
sources=(Document,)
)
menu_list_facet.bind_links(
links=(
link_acl_list, link_events_for_object,
link_web_link_document_types,
link_object_event_types_user_subcriptions_list,
), sources=(WebLink,)
)
menu_list_facet.bind_links(
links=(link_document_type_web_links,), sources=(DocumentType,)
)
menu_object.bind_links(
links=(
link_web_link_delete, link_web_link_edit
), sources=(WebLink,)
)
menu_object.bind_links(
links=(link_web_link_instance_view,),
sources=(ResolvedWebLink,)
)
menu_secondary.bind_links(
links=(link_web_link_list, link_web_link_create),
sources=(
WebLink, 'linking:web_link_list',
'linking:web_link_create'
)
)
menu_setup.bind_links(links=(link_web_link_setup,))
registry.register(WebLink)

View File

@@ -0,0 +1,16 @@
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=_('Web links'), name='linking'
)
event_web_link_created = namespace.add_event_type(
label=_('Web link created'), name='web_link_created'
)
event_web_link_edited = namespace.add_event_type(
label=_('Web link edited'), name='web_link_edited'
)

View File

@@ -0,0 +1,11 @@
from __future__ import unicode_literals
from django import forms
from .models import WebLink
class WebLinkForm(forms.ModelForm):
class Meta:
fields = ('label', 'template', 'enabled')
model = WebLink

View File

@@ -0,0 +1,22 @@
from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
from mayan.apps.documents.icons import icon_document_type
icon_web_link = Icon(driver_name='fontawesome', symbol='external-link-alt')
icon_document_type_web_links = icon_web_link
icon_document_web_link_list = Icon(
driver_name='fontawesome', symbol='external-link-alt'
)
icon_web_link_create = Icon(
driver_name='fontawesome-dual', primary_symbol='external-link-alt',
secondary_symbol='plus'
)
icon_web_link_delete = Icon(driver_name='fontawesome', symbol='times')
icon_web_link_document_types = icon_document_type
icon_web_link_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_web_link_instance_view = Icon(
driver_name='fontawesome', symbol='external-link-alt'
)
icon_web_link_setup = icon_web_link
icon_web_link_list = icon_web_link

View File

@@ -0,0 +1,63 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.documents.permissions import permission_document_type_edit
from mayan.apps.navigation.classes import Link
from .permissions import (
permission_web_link_create, permission_web_link_delete,
permission_web_link_edit, permission_web_link_instance_view,
permission_web_link_view
)
link_document_type_web_links = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.weblinks.icons.icon_document_type_web_links',
permissions=(permission_document_type_edit,), text=_('Web links'),
view='weblinks:document_type_web_links',
)
link_web_link_create = Link(
icon_class_path='mayan.apps.weblinks.icons.icon_web_link_create',
permissions=(permission_web_link_create,),
text=_('Create new web link'), view='weblinks:web_link_create'
)
link_web_link_delete = Link(
args='object.pk',
icon_class_path='mayan.apps.weblinks.icons.icon_web_link_delete',
permissions=(permission_web_link_delete,),
tags='dangerous', text=_('Delete'), view='weblinks:web_link_delete',
)
link_web_link_document_types = Link(
args='object.pk',
icon_class_path='mayan.apps.weblinks.icons.icon_web_link_document_types',
permissions=(permission_web_link_edit,),
text=_('Document types'), view='weblinks:web_link_document_types',
)
link_web_link_edit = Link(
args='object.pk',
icon_class_path='mayan.apps.weblinks.icons.icon_web_link_edit',
permissions=(permission_web_link_edit,),
text=_('Edit'), view='weblinks:web_link_edit',
)
link_web_link_instance_view = Link(
icon_class_path='mayan.apps.weblinks.icons.icon_web_link_instance_view',
args=('document.pk', 'object.pk',),
permissions=(permission_web_link_instance_view,), tags='new_window',
text=_('Navigate'), view='weblinks:web_link_instance_view',
)
link_document_web_link_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.weblinks.icons.icon_document_web_link_list',
permissions=(permission_web_link_instance_view,), text=_('Web links'),
view='weblinks:document_web_link_list',
)
link_web_link_list = Link(
icon_class_path='mayan.apps.weblinks.icons.icon_web_link_list',
text=_('Web links'), view='weblinks:web_link_list'
)
link_web_link_setup = Link(
icon_class_path='mayan.apps.weblinks.icons.icon_web_link_setup',
permissions=(permission_web_link_create,), text=_('Web links'),
view='weblinks:web_link_list'
)

View File

@@ -0,0 +1,16 @@
from django.db import models
from mayan.apps.acls.models import AccessControlList
from .permissions import permission_web_link_instance_view
class WebLinkManager(models.Manager):
def get_for(self, document, user):
queryset = self.filter(
document_types=document.document_type, enabled=True
)
return AccessControlList.objects.restrict_queryset(
permission=permission_web_link_instance_view,
queryset=queryset, user=user
)

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-01 19:42
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('documents', '0047_auto_20180917_0737'),
]
operations = [
migrations.CreateModel(
name='WebLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(db_index=True, max_length=96, verbose_name='Label')),
('template', models.TextField(verbose_name='Template')),
('enabled', models.BooleanField(default=True, verbose_name='Enabled')),
('document_types', models.ManyToManyField(related_name='web_links', to='documents.DocumentType', verbose_name='Document types')),
],
options={
'ordering': ('label',),
'verbose_name': 'Web link',
'verbose_name_plural': 'Web links',
},
),
migrations.CreateModel(
name='ResolvedWebLink',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('weblinks.weblink',),
),
]

View File

@@ -0,0 +1,90 @@
from __future__ import unicode_literals
from django.db import models, transaction
from django.template import Context, Template
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import DocumentType
from .events import event_web_link_created, event_web_link_edited
from .managers import WebLinkManager
@python_2_unicode_compatible
class WebLink(models.Model):
"""
This model stores the basic fields for a web link. Web links allow
generating links from documents to external resources.
"""
label = models.CharField(
db_index=True, max_length=96, verbose_name=_('Label')
)
template = models.TextField(verbose_name=_('Template'))
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
document_types = models.ManyToManyField(
related_name='web_links', to=DocumentType,
verbose_name=_('Document types')
)
objects = WebLinkManager()
class Meta:
ordering = ('label',)
verbose_name = _('Web link')
verbose_name_plural = _('Web links')
def __str__(self):
return self.label
def document_types_add(self, queryset, _user=None):
with transaction.atomic():
event_web_link_edited.commit(
actor=_user, target=self
)
for obj in queryset:
self.document_types.add(obj)
event_document_type_edited.commit(
actor=_user, action_object=self, target=obj
)
def document_types_remove(self, queryset, _user=None):
with transaction.atomic():
event_web_link_edited.commit(
actor=_user, target=self
)
for obj in queryset:
self.document_types.remove(obj)
event_document_type_edited.commit(
actor=_user, action_object=self, target=obj
)
def save(self, *args, **kwargs):
_user = kwargs.pop('_user', None)
with transaction.atomic():
is_new = not self.pk
super(WebLink, self).save(*args, **kwargs)
if is_new:
event_web_link_created.commit(
actor=_user, target=self
)
else:
event_web_link_edited.commit(
actor=_user, target=self
)
class ResolvedWebLink(WebLink):
"""
Proxy model to represent an already resolved web link. Used for easier
colums registration.
"""
class Meta:
proxy = True
def get_url_for(self, document):
context = Context({'document': document})
return Template(self.template).render(context=context)

View File

@@ -0,0 +1,23 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Web links'), name='web_links')
permission_web_link_create = namespace.add_permission(
label=_('Create new web links'), name='web_link_create'
)
permission_web_link_delete = namespace.add_permission(
label=_('Delete web links'), name='web_link_delete'
)
permission_web_link_edit = namespace.add_permission(
label=_('Edit web links'), name='web_link_edit'
)
permission_web_link_view = namespace.add_permission(
label=_('View existing web links'), name='web_link_view'
)
permission_web_link_instance_view = namespace.add_permission(
label=_('View web link instances'), name='web_link_instance_view'
)

View File

@@ -0,0 +1,47 @@
from __future__ import unicode_literals
from django.conf.urls import url
from .views import (
DocumentWebLinkListView, DocumentTypeWebLinksView, ResolvedWebLinkView,
SetupWebLinkDocumentTypesView, WebLinkCreateView, WebLinkDeleteView,
WebLinkEditView, WebLinkListView
)
urlpatterns = [
url(
regex=r'^document/(?P<pk>\d+)/list/$',
view=DocumentWebLinkListView.as_view(),
name='document_web_link_list'
),
url(
regex=r'^document/(?P<document_pk>\d+)/(?P<web_link_pk>\d+)/$',
view=ResolvedWebLinkView.as_view(), name='web_link_instance_view'
),
url(
regex=r'^document_types/(?P<pk>\d+)/web_links/$',
view=DocumentTypeWebLinksView.as_view(),
name='document_type_web_links'
),
url(
regex=r'^setup/list/$', view=WebLinkListView.as_view(),
name='web_link_list'
),
url(
regex=r'^setup/create/$', view=WebLinkCreateView.as_view(),
name='web_link_create'
),
url(
regex=r'^setup/(?P<pk>\d+)/delete/$',
view=WebLinkDeleteView.as_view(), name='web_link_delete'
),
url(
regex=r'^setup/(?P<pk>\d+)/edit/$', view=WebLinkEditView.as_view(),
name='web_link_edit'
),
url(
regex=r'^setup/(?P<pk>\d+)/document_types/$',
view=SetupWebLinkDocumentTypesView.as_view(),
name='web_link_document_types'
),
]

View File

@@ -0,0 +1,233 @@
from __future__ import absolute_import, unicode_literals
import logging
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.views.generic import RedirectView
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectEditView, SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.permissions import permission_document_type_edit
from .events import event_web_link_edited
from .forms import WebLinkForm
from .icons import icon_web_link_setup
from .links import link_web_link_create
from .models import ResolvedWebLink, WebLink
from .permissions import (
permission_web_link_create, permission_web_link_delete,
permission_web_link_edit, permission_web_link_instance_view,
permission_web_link_view
)
logger = logging.getLogger(__name__)
class DocumentTypeWebLinksView(AddRemoveView):
main_object_method_add = 'web_link_add'
main_object_method_remove = 'web_link_remove'
main_object_permission = permission_document_type_edit
main_object_model = DocumentType
main_object_pk_url_kwarg = 'pk'
secondary_object_model = WebLink
secondary_object_permission = permission_web_link_edit
list_available_title = _('Available web links')
list_added_title = _('Web links enabled')
related_field = 'web_links'
def action_add(self, queryset, _user):
with transaction.atomic():
event_document_type_edited.commit(
actor=_user, target=self.main_object
)
for obj in queryset:
self.main_object.web_links.add(obj)
event_web_link_edited.commit(
actor=_user, action_object=self.main_object, target=obj
)
def action_remove(self, queryset, _user):
with transaction.atomic():
event_document_type_edited.commit(
actor=_user, target=self.main_object
)
for obj in queryset:
self.main_object.web_links.remove(obj)
event_web_link_edited.commit(
actor=_user, action_object=self.main_object, target=obj
)
def get_actions_extra_kwargs(self):
return {'_user': self.request.user}
def get_extra_context(self):
return {
'object': self.main_object,
'title': _(
'Web links to enable for document type: %s'
) % self.main_object,
}
class ResolvedWebLinkView(ExternalObjectMixin, RedirectView):
external_object_class = Document
external_object_pk_url_kwarg = 'document_pk'
external_object_permission = permission_web_link_instance_view
def get_redirect_url(self, *args, **kwargs):
return self.get_resolved_web_link().get_url_for(
document=self.external_object
)
def get_resolved_web_link(self):
return get_object_or_404(
klass=self.get_web_link_queryset(), pk=self.kwargs['web_link_pk']
)
def get_web_link_queryset(self):
return ResolvedWebLink.objects.get_for(
document=self.external_object, user=self.request.user
)
class SetupWebLinkDocumentTypesView(AddRemoveView):
main_object_method_add = 'document_types_add'
main_object_method_remove = 'document_types_remove'
main_object_permission = permission_web_link_edit
main_object_model = WebLink
main_object_pk_url_kwarg = 'pk'
secondary_object_model = DocumentType
secondary_object_permission = permission_document_type_edit
list_available_title = _('Available document types')
list_added_title = _('Document types enabled')
related_field = 'document_types'
def get_actions_extra_kwargs(self):
return {'_user': self.request.user}
def get_extra_context(self):
return {
'object': self.main_object,
'title': _(
'Document type for which to enable web link: %s'
) % self.main_object,
}
class WebLinkListView(SingleObjectListView):
object_permission = permission_web_link_view
def get_extra_context(self):
return {
'hide_link': True,
'hide_object': True,
'no_results_icon': icon_web_link_setup,
'no_results_main_link': link_web_link_create.resolve(
context=RequestContext(request=self.request)
),
'no_results_text': _(
'Web links allow generating links from documents to external '
'resources.'
),
'no_results_title': _(
'There are no web links'
),
'title': _('Web links'),
}
def get_source_queryset(self):
return self.get_web_link_queryset()
def get_web_link_queryset(self):
return WebLink.objects.all()
class DocumentWebLinkListView(WebLinkListView):
def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
obj=self.document, permissions=(permission_web_link_instance_view,),
user=request.user
)
return super(
DocumentWebLinkListView, self
).dispatch(request, *args, **kwargs)
def get_extra_context(self):
return {
'document': self.document,
'hide_link': True,
'hide_object': True,
'no_results_icon': icon_web_link_setup,
#'no_results_text': _(
# 'Web links allow defining relationships between '
# 'documents even if they are in different indexes and '
# 'are of different types.'
#),
'no_results_title': _(
'There are no web links for this document'
),
'object': self.document,
'title': _('Web links for document: %s') % self.document,
}
def get_web_link_queryset(self):
return ResolvedWebLink.objects.get_for(
document=self.document, user=self.request.user
)
class WebLinkCreateView(SingleObjectCreateView):
extra_context = {'title': _('Create new web link')}
form_class = WebLinkForm
post_action_redirect = reverse_lazy(
viewname='weblinks:web_link_list'
)
view_permission = permission_web_link_create
def get_save_extra_data(self):
return {'_user': self.request.user}
class WebLinkDeleteView(SingleObjectDeleteView):
model = WebLink
object_permission = permission_web_link_delete
post_action_redirect = reverse_lazy(
viewname='weblinks:web_link_list'
)
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _('Delete web link: %s') % self.get_object()
}
class WebLinkEditView(SingleObjectEditView):
form_class = WebLinkForm
model = WebLink
object_permission = permission_web_link_edit
post_action_redirect = reverse_lazy(
viewname='weblinks:web_link_list'
)
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _('Edit web link: %s') % self.get_object()
}
def get_save_extra_data(self):
return {'_user': self.request.user}

View File

@@ -117,6 +117,7 @@ INSTALLED_APPS = (
'mayan.apps.document_states',
'mayan.apps.documents',
'mayan.apps.file_metadata',
'mayan.apps.importer',
'mayan.apps.linking',
'mayan.apps.mailer',
'mayan.apps.mayan_statistics',