Compare commits
89 Commits
versions/m
...
clients/bc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a99a9df3e | ||
|
|
f0ca92c06b | ||
|
|
69086d87dd | ||
|
|
89f05aeaa1 | ||
|
|
a966d6c8cf | ||
|
|
cf18e99caa | ||
|
|
f87454c0b6 | ||
|
|
b7febc8df5 | ||
|
|
f4b34bf48d | ||
|
|
d2fd865b68 | ||
|
|
a5e00ceba9 | ||
|
|
f09fec0aff | ||
|
|
ce7c805251 | ||
|
|
1bcc9332b2 | ||
|
|
e3267d3973 | ||
|
|
eb7cbc73ee | ||
|
|
596b5ccf67 | ||
|
|
34838a438d | ||
|
|
572690e2bc | ||
|
|
303e34299a | ||
|
|
c628de9ede | ||
|
|
e73be6bbab | ||
|
|
c9fd8b02e3 | ||
|
|
e1a63064dc | ||
|
|
42db8255d1 | ||
|
|
22ba6cfb49 | ||
|
|
02bba73ca7 | ||
|
|
d0daf559c7 | ||
|
|
f8f6700459 | ||
|
|
c318d37445 | ||
|
|
246fc15988 | ||
|
|
14d45cbe90 | ||
|
|
42a544c6e3 | ||
|
|
75be11bc96 | ||
|
|
ebf29d0eed | ||
|
|
a391d27b44 | ||
|
|
753c9b8b4b | ||
|
|
9cb6c6599d | ||
|
|
8bd0d0166b | ||
|
|
bee0c0b189 | ||
|
|
744bfefa5c | ||
|
|
850fb16c8c | ||
|
|
72ba805fbb | ||
|
|
3d7b40f029 | ||
|
|
2039a9f13b | ||
|
|
bb8f12dd7a | ||
|
|
40ab1f3665 | ||
|
|
fdef757fd0 | ||
|
|
3608ee1141 | ||
|
|
7fb3d61dff | ||
|
|
e9aa11673b | ||
|
|
03a7aa5daf | ||
|
|
755f20c5c4 | ||
|
|
64772e2e90 | ||
|
|
75a4a426e0 | ||
|
|
42a7ebeea2 | ||
|
|
3d22f48555 | ||
|
|
488e048d8f | ||
|
|
2f82559a5c | ||
|
|
7d5b7b9fc4 | ||
|
|
7aa68b8bbf | ||
|
|
aecde926f2 | ||
|
|
6b95628e56 | ||
|
|
56a1b97b46 | ||
|
|
34a5a54c8b | ||
|
|
0c17ab3f8a | ||
|
|
c967a25f82 | ||
|
|
7562588c42 | ||
|
|
a1a706b7b9 | ||
|
|
d623cb2df5 | ||
|
|
488ddcf1e1 | ||
|
|
3d39893f17 | ||
|
|
3694839d97 | ||
|
|
cce27aceca | ||
|
|
c73d251370 | ||
|
|
091f0d1cfd | ||
|
|
d2affdcf21 | ||
|
|
885d430b98 | ||
|
|
39eabe1c54 | ||
|
|
f6ad579829 | ||
|
|
6fc9e46882 | ||
|
|
2d326a679d | ||
|
|
aa8c2db446 | ||
|
|
925b55d76d | ||
|
|
5808d3653d | ||
|
|
bc072f7b7e | ||
|
|
b3d59eee39 | ||
|
|
7d379a52af | ||
|
|
499ab1f3e7 |
@@ -63,6 +63,7 @@ job_docker_nightly:
|
|||||||
only:
|
only:
|
||||||
- nightly
|
- nightly
|
||||||
- staging
|
- staging
|
||||||
|
- /^clients\/.+$/
|
||||||
|
|
||||||
job_documentation_build:
|
job_documentation_build:
|
||||||
stage: build_documentation
|
stage: build_documentation
|
||||||
@@ -152,7 +153,7 @@ job_push_python:
|
|||||||
- locale-gen en_US.UTF-8
|
- locale-gen en_US.UTF-8
|
||||||
- update-locale LANG=en_US.UTF-8
|
- update-locale LANG=en_US.UTF-8
|
||||||
- export LC_ALL=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
|
- virtualenv venv -p /usr/bin/python3
|
||||||
- . venv/bin/activate
|
- . venv/bin/activate
|
||||||
- pip install -r requirements.txt -r requirements/testing-base.txt
|
- pip install -r requirements.txt -r requirements/testing-base.txt
|
||||||
@@ -162,6 +163,7 @@ job_push_python:
|
|||||||
- releases/python
|
- releases/python
|
||||||
- staging
|
- staging
|
||||||
- nightly
|
- nightly
|
||||||
|
- /^clients\/.+$/
|
||||||
|
|
||||||
test-mysql:
|
test-mysql:
|
||||||
<<: *test_base
|
<<: *test_base
|
||||||
|
|||||||
20
CHANGES_BC.rst
Normal file
20
CHANGES_BC.rst
Normal 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)
|
||||||
@@ -78,11 +78,6 @@
|
|||||||
Support Docker networks and make it the default.
|
Support Docker networks and make it the default.
|
||||||
Delete the containers to allow the script to be idempotent.
|
Delete the containers to allow the script to be idempotent.
|
||||||
Deploy a Redis container.
|
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)
|
3.2.8 (2019-10-01)
|
||||||
==================
|
==================
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ Changes
|
|||||||
GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
|
GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
|
||||||
for the report and the research.
|
for the report and the research.
|
||||||
|
|
||||||
|
|
||||||
Removals
|
Removals
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Changes
|
|||||||
incorrectly state it is named flag_list. Closes GitLab issue
|
incorrectly state it is named flag_list. Closes GitLab issue
|
||||||
#606. Thanks to Samuel Aebi (@samuelaebi) for the report and
|
#606. Thanks to Samuel Aebi (@samuelaebi) for the report and
|
||||||
debug information.
|
debug information.
|
||||||
|
debug information.
|
||||||
- Support configurable GUnicorn timeouts. Defaults to
|
- Support configurable GUnicorn timeouts. Defaults to
|
||||||
current value of 120 seconds.
|
current value of 120 seconds.
|
||||||
- Fix help text of the platformtemplate command.
|
- Fix help text of the platformtemplate command.
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ Changes
|
|||||||
Removals
|
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
|
provided by this feature (SQLite to PostgreSQL) was being confused with
|
||||||
database migrations and upgrades.
|
database migrations and upgrades.
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ Removals
|
|||||||
|
|
||||||
Continued confusion about the purpose of the feature and confusion about
|
Continued confusion about the purpose of the feature and confusion about
|
||||||
how errors with this feature were a reflexion of the code quality of
|
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
|
- Django environ
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
__title__ = 'Mayan EDMS'
|
__title__ = 'Mayan EDMS'
|
||||||
__version__ = '3.3beta1'
|
__version__ = '3.3beta1'
|
||||||
__build__ = 0x030300
|
__build__ = 0x030208
|
||||||
__build_string__ = 'v3.3beta1-9-g1b327b99f0_Tue Oct 8 15:15:08 2019 -0400'
|
__build_string__ = 'v3.2.8-255-g69086d87dd_Tue Oct 8 09:43:10 2019 -0400'
|
||||||
__django_version__ = '1.11'
|
__django_version__ = '1.11'
|
||||||
__author__ = 'Roberto Rosario'
|
__author__ = 'Roberto Rosario'
|
||||||
__author_email__ = 'roberto.rosario@mayan-edms.com'
|
__author_email__ = 'roberto.rosario@mayan-edms.com'
|
||||||
|
|||||||
19
mayan/apps/authentication/events.py
Normal file
19
mayan/apps/authentication/events.py
Normal 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'
|
||||||
|
)
|
||||||
82
mayan/apps/authentication/tests/test_events.py
Normal file
82
mayan/apps/authentication/tests/test_events.py
Normal 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)
|
||||||
@@ -21,8 +21,13 @@ from mayan.apps.common.generics import MultipleObjectFormActionView
|
|||||||
from mayan.apps.common.settings import (
|
from mayan.apps.common.settings import (
|
||||||
setting_home_view, setting_project_title, setting_project_url
|
setting_home_view, setting_project_title, setting_project_url
|
||||||
)
|
)
|
||||||
|
from mayan.apps.events.utils import get_system_user
|
||||||
from mayan.apps.user_management.permissions import permission_user_edit
|
from mayan.apps.user_management.permissions import permission_user_edit
|
||||||
|
|
||||||
|
from .events import (
|
||||||
|
event_user_authentication_error, event_user_password_reset_complete,
|
||||||
|
event_user_password_reset_started
|
||||||
|
)
|
||||||
from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
|
from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
|
||||||
from .settings import setting_login_method, setting_maximum_session_length
|
from .settings import setting_login_method, setting_maximum_session_length
|
||||||
|
|
||||||
@@ -57,6 +62,10 @@ class MayanLoginView(StrongholdPublicMixin, LoginView):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
event_user_authentication_error.commit(actor=get_system_user())
|
||||||
|
return super(MayanLoginView, self).form_invalid(form=form)
|
||||||
|
|
||||||
def get_form_class(self):
|
def get_form_class(self):
|
||||||
if setting_login_method.value == 'email':
|
if setting_login_method.value == 'email':
|
||||||
return EmailAuthenticationForm
|
return EmailAuthenticationForm
|
||||||
@@ -112,6 +121,10 @@ class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmV
|
|||||||
)
|
)
|
||||||
template_name = 'authentication/password_reset_confirm.html'
|
template_name = 'authentication/password_reset_confirm.html'
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
event_user_password_reset_complete.commit(actor=get_system_user())
|
||||||
|
return super(MayanPasswordResetConfirmView, self).post(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
|
class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
|
||||||
extra_context = {
|
extra_context = {
|
||||||
@@ -137,6 +150,10 @@ class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
|
|||||||
)
|
)
|
||||||
template_name = 'authentication/password_reset_form.html'
|
template_name = 'authentication/password_reset_form.html'
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
event_user_password_reset_started.commit(actor=get_system_user())
|
||||||
|
return super(MayanPasswordResetView, self).post(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class UserSetPasswordView(MultipleObjectFormActionView):
|
class UserSetPasswordView(MultipleObjectFormActionView):
|
||||||
form_class = SetPasswordForm
|
form_class = SetPasswordForm
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class CabinetDocumentUploadTestCase(CabinetTestMixin, GenericDocumentViewTestCas
|
|||||||
def _request_upload_interactive_document_create_view(self):
|
def _request_upload_interactive_document_create_view(self):
|
||||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||||
return self.post(
|
return self.post(
|
||||||
viewname='sources:document_upload_interactive', kwargs={
|
viewname='sources:upload_interactive', kwargs={
|
||||||
'source_id': self.test_source.pk
|
'source_id': self.test_source.pk
|
||||||
}, data={
|
}, data={
|
||||||
'document_type_id': self.test_document_type.pk,
|
'document_type_id': self.test_document_type.pk,
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ class NewVersionBlockViewTestCase(
|
|||||||
self.login_superuser()
|
self.login_superuser()
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
viewname='sources:document_version_upload', kwargs={
|
viewname='sources:upload_version', kwargs={
|
||||||
'document_pk': self.test_document.pk
|
'document_pk': self.test_document.pk
|
||||||
}, follow=True
|
}, follow=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ class MultiFormView(DjangoFormView):
|
|||||||
template_name = 'appearance/generic_form.html'
|
template_name = 'appearance/generic_form.html'
|
||||||
|
|
||||||
def _create_form(self, form_name, klass):
|
def _create_form(self, form_name, klass):
|
||||||
form_kwargs = self.get_form_kwargs(form_name=form_name)
|
form_kwargs = self.get_form_kwargs(form_name)
|
||||||
form_create_method = 'create_{}_form'.format(form_name)
|
form_create_method = 'create_%s_form' % form_name
|
||||||
if hasattr(self, form_create_method):
|
if hasattr(self, form_create_method):
|
||||||
form = getattr(self, form_create_method)(**form_kwargs)
|
form = getattr(self, form_create_method)(**form_kwargs)
|
||||||
else:
|
else:
|
||||||
@@ -66,17 +66,17 @@ class MultiFormView(DjangoFormView):
|
|||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
form_classes = self.get_form_classes()
|
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)
|
return super(MultiFormView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def forms_valid(self, forms):
|
def forms_valid(self, forms):
|
||||||
for form_name, form in forms.items():
|
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):
|
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())
|
return HttpResponseRedirect(redirect_to=self.get_success_url())
|
||||||
|
|
||||||
@@ -98,16 +98,14 @@ class MultiFormView(DjangoFormView):
|
|||||||
|
|
||||||
def get_form_kwargs(self, form_name):
|
def get_form_kwargs(self, form_name):
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
kwargs.update({'initial': self.get_initial(form_name=form_name)})
|
kwargs.update({'initial': self.get_initial(form_name)})
|
||||||
kwargs.update({'prefix': self.get_prefix(form_name=form_name)})
|
kwargs.update({'prefix': self.get_prefix(form_name)})
|
||||||
|
|
||||||
if self.request.method in ('POST', 'PUT'):
|
if self.request.method in ('POST', 'PUT'):
|
||||||
kwargs.update(
|
kwargs.update({
|
||||||
{
|
'data': self.request.POST,
|
||||||
'data': self.request.POST,
|
'files': self.request.FILES,
|
||||||
'files': self.request.FILES,
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
kwargs.update(self.get_form_extra_kwargs(form_name=form_name) or {})
|
kwargs.update(self.get_form_extra_kwargs(form_name=form_name) or {})
|
||||||
|
|
||||||
@@ -120,13 +118,13 @@ class MultiFormView(DjangoFormView):
|
|||||||
return dict(
|
return dict(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
key, self._create_form(form_name=key, klass=klass)
|
key, self._create_form(key, klass)
|
||||||
) for key, klass in form_classes.items()
|
) for key, klass in form_classes.items()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_initial(self, form_name):
|
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):
|
if hasattr(self, initial_method):
|
||||||
return getattr(self, initial_method)()
|
return getattr(self, initial_method)()
|
||||||
else:
|
else:
|
||||||
@@ -208,9 +206,9 @@ class AddRemoveView(
|
|||||||
getattr(self.main_object, self.related_field).add(*queryset)
|
getattr(self.main_object, self.related_field).add(*queryset)
|
||||||
else:
|
else:
|
||||||
raise ImproperlyConfigured(
|
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 '
|
'related_field, or an action_add '
|
||||||
'method.'.format(self.__class__.__name__)
|
'method.' % self.__class__.__name__
|
||||||
)
|
)
|
||||||
|
|
||||||
def _action_remove(self, queryset):
|
def _action_remove(self, queryset):
|
||||||
@@ -227,9 +225,9 @@ class AddRemoveView(
|
|||||||
getattr(self.main_object, self.related_field).remove(*queryset)
|
getattr(self.main_object, self.related_field).remove(*queryset)
|
||||||
else:
|
else:
|
||||||
raise ImproperlyConfigured(
|
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 '
|
'related_field, or an action_remove '
|
||||||
'method.'.format(self.__class__.__name__)
|
'method.' % self.__class__.__name__
|
||||||
)
|
)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
@@ -350,10 +348,8 @@ class AddRemoveView(
|
|||||||
def get_list_added_queryset(self):
|
def get_list_added_queryset(self):
|
||||||
if not self.related_field:
|
if not self.related_field:
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
'View {} must be called with either a related_field or '
|
'View %s must be called with either a related_field or '
|
||||||
'override .get_list_added_queryset().'.format(
|
'override .get_list_added_queryset().' % self.__class__.__name__
|
||||||
self.__class__.__name__
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.get_secondary_object_list().filter(
|
return self.get_secondary_object_list().filter(
|
||||||
|
|||||||
@@ -199,3 +199,36 @@ class IndexToolsViewTestCase(
|
|||||||
|
|
||||||
# An instance root exists
|
# An instance root exists
|
||||||
self.assertTrue(self.test_index.instance_root.pk)
|
self.assertTrue(self.test_index.instance_root.pk)
|
||||||
|
|
||||||
|
def test_index_rebuild_view_no_permission(self):
|
||||||
|
self._create_test_index()
|
||||||
|
|
||||||
|
self.test_index.node_templates.create(
|
||||||
|
parent=self.test_index.template_root,
|
||||||
|
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
|
||||||
|
link_documents=True
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self._request_test_index_rebuild_view()
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
self.assertEqual(IndexInstanceNode.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_index_rebuild_view_with_access(self):
|
||||||
|
self._create_test_index()
|
||||||
|
|
||||||
|
self.test_index.node_templates.create(
|
||||||
|
parent=self.test_index.template_root,
|
||||||
|
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
|
||||||
|
link_documents=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.grant_access(
|
||||||
|
obj=self.test_index,
|
||||||
|
permission=permission_document_indexing_rebuild
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self._request_test_index_rebuild_view()
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
self.assertNotEqual(IndexInstanceNode.objects.count(), 0)
|
||||||
|
|||||||
@@ -205,10 +205,10 @@ class DocumentStatesApp(MayanAppConfig):
|
|||||||
|
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
source=WorkflowInstanceLogEntry, label=_('Date and time'),
|
source=WorkflowInstanceLogEntry, label=_('Date and time'),
|
||||||
attribute='datetime'
|
attribute='datetime', is_sortable=True
|
||||||
)
|
)
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
source=WorkflowInstanceLogEntry, label=_('User'), attribute='user'
|
source=WorkflowInstanceLogEntry, attribute='user', is_sortable=True
|
||||||
)
|
)
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
source=WorkflowInstanceLogEntry,
|
source=WorkflowInstanceLogEntry,
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ link_workflow_template_transition_field_delete = Link(
|
|||||||
tags='dangerous', text=_('Delete'),
|
tags='dangerous', text=_('Delete'),
|
||||||
view='document_states:workflow_template_transition_field_delete',
|
view='document_states:workflow_template_transition_field_delete',
|
||||||
)
|
)
|
||||||
|
|
||||||
link_workflow_template_transition_field_edit = Link(
|
link_workflow_template_transition_field_edit = Link(
|
||||||
args='resolved_object.pk',
|
args='resolved_object.pk',
|
||||||
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_edit',
|
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_edit',
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import logging
|
|||||||
|
|
||||||
from furl import furl
|
from furl import furl
|
||||||
from graphviz import Digraph
|
from graphviz import Digraph
|
||||||
|
import yaml
|
||||||
|
try:
|
||||||
|
from yaml import CSafeLoader as SafeLoader
|
||||||
|
except ImportError:
|
||||||
|
from yaml import SafeLoader
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -328,8 +333,8 @@ class WorkflowState(models.Model):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Solve issue #557 "Break workflows with invalid input"
|
# Solve issue #557 "Break workflows with invalid input"
|
||||||
# without using a migration.
|
# without using a migration.
|
||||||
# Remove blank=True, remove this, and create a migration in the next
|
# TODO: Remove blank=True, remove this, and create a migration in the
|
||||||
# minor version.
|
# next minor version.
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.completion = int(self.completion)
|
self.completion = int(self.completion)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
from mayan.apps.common.tests.base import GenericViewTestCase
|
from mayan.apps.common.tests.base import GenericViewTestCase
|
||||||
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
|
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
|
||||||
|
|
||||||
|
from ..literals import FIELD_TYPE_CHOICE_CHAR
|
||||||
from ..models import WorkflowTransition
|
from ..models import WorkflowTransition
|
||||||
from ..permissions import (
|
from ..permissions import (
|
||||||
permission_workflow_edit, permission_workflow_view,
|
permission_workflow_edit, permission_workflow_view,
|
||||||
@@ -19,6 +20,11 @@ from .mixins import (
|
|||||||
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin
|
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TEST_WORKFLOW_TRANSITION_FIELD_NAME = 'test_workflow_transition_field'
|
||||||
|
TEST_WORKFLOW_TRANSITION_FIELD_LABEL = 'test workflow transition field'
|
||||||
|
TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT = 'test workflow transition field help test'
|
||||||
|
TEST_WORKFLOW_TRANSITION_FIELD_TYPE = FIELD_TYPE_CHOICE_CHAR
|
||||||
|
|
||||||
|
|
||||||
class WorkflowTransitionViewTestCase(
|
class WorkflowTransitionViewTestCase(
|
||||||
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin,
|
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin,
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ from .permissions import (
|
|||||||
permission_document_download, permission_document_edit,
|
permission_document_download, permission_document_edit,
|
||||||
permission_document_new_version, permission_document_print,
|
permission_document_new_version, permission_document_print,
|
||||||
permission_document_properties_edit, permission_document_restore,
|
permission_document_properties_edit, permission_document_restore,
|
||||||
permission_document_tools, permission_document_trash,
|
permission_document_trash, permission_document_type_delete,
|
||||||
permission_document_type_delete, permission_document_type_edit,
|
permission_document_type_edit, permission_document_type_view,
|
||||||
permission_document_type_view, permission_document_version_revert,
|
permission_document_version_revert, permission_document_version_view,
|
||||||
permission_document_version_view, permission_document_view
|
permission_document_view
|
||||||
)
|
)
|
||||||
# Just import to initialize the search models
|
# Just import to initialize the search models
|
||||||
from .search import document_search, document_page_search # NOQA
|
from .search import document_search, document_page_search # NOQA
|
||||||
@@ -191,8 +191,8 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
permission_document_delete, permission_document_download,
|
permission_document_delete, permission_document_download,
|
||||||
permission_document_edit, permission_document_new_version,
|
permission_document_edit, permission_document_new_version,
|
||||||
permission_document_print, permission_document_properties_edit,
|
permission_document_print, permission_document_properties_edit,
|
||||||
permission_document_restore, permission_document_tools,
|
permission_document_restore, permission_document_trash,
|
||||||
permission_document_trash, permission_document_version_revert,
|
permission_document_version_revert,
|
||||||
permission_document_version_view, permission_document_view,
|
permission_document_version_view, permission_document_view,
|
||||||
permission_events_view, permission_transformation_create,
|
permission_events_view, permission_transformation_create,
|
||||||
permission_transformation_delete,
|
permission_transformation_delete,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ DOCUMENT_IMAGES_CACHE_NAME = 'document_images'
|
|||||||
DOCUMENT_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.documents.storages.storage_documentimagecache'
|
DOCUMENT_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.documents.storages.storage_documentimagecache'
|
||||||
STUB_EXPIRATION_INTERVAL = 60 * 60 * 24 # 24 hours
|
STUB_EXPIRATION_INTERVAL = 60 * 60 * 24 # 24 hours
|
||||||
UPDATE_PAGE_COUNT_RETRY_DELAY = 10
|
UPDATE_PAGE_COUNT_RETRY_DELAY = 10
|
||||||
|
UPLOAD_NEW_DOCUMENT_RETRY_DELAY = 10
|
||||||
UPLOAD_NEW_VERSION_RETRY_DELAY = 10
|
UPLOAD_NEW_VERSION_RETRY_DELAY = 10
|
||||||
|
|
||||||
PAGE_RANGE_ALL = 'all'
|
PAGE_RANGE_ALL = 'all'
|
||||||
|
|||||||
@@ -102,14 +102,6 @@ class Document(models.Model):
|
|||||||
)
|
)
|
||||||
return RecentDocument.objects.add_document_for_user(user, self)
|
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):
|
def delete(self, *args, **kwargs):
|
||||||
to_trash = kwargs.pop('to_trash', True)
|
to_trash = kwargs.pop('to_trash', True)
|
||||||
|
|
||||||
@@ -134,14 +126,6 @@ class Document(models.Model):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
|
||||||
def file_mime_encoding(self):
|
|
||||||
return self.latest_version.encoding
|
|
||||||
|
|
||||||
@property
|
|
||||||
def file_mimetype(self):
|
|
||||||
return self.latest_version.mimetype
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
viewname='documents:document_preview', kwargs={'pk': self.pk}
|
viewname='documents:document_preview', kwargs={'pk': self.pk}
|
||||||
@@ -156,10 +140,6 @@ class Document(models.Model):
|
|||||||
def is_in_trash(self):
|
def is_in_trash(self):
|
||||||
return self.in_trash
|
return self.in_trash
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_version(self):
|
|
||||||
return self.versions.order_by('timestamp').last()
|
|
||||||
|
|
||||||
def natural_key(self):
|
def natural_key(self):
|
||||||
return (self.uuid,)
|
return (self.uuid,)
|
||||||
natural_key.dependencies = ['documents.DocumentType']
|
natural_key.dependencies = ['documents.DocumentType']
|
||||||
@@ -185,34 +165,6 @@ class Document(models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.latest_version.open(*args, **kwargs)
|
return self.latest_version.open(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
|
||||||
def page_count(self):
|
|
||||||
return self.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):
|
def restore(self):
|
||||||
self.in_trash = False
|
self.in_trash = False
|
||||||
self.save()
|
self.save()
|
||||||
@@ -257,3 +209,53 @@ class Document(models.Model):
|
|||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
return self.latest_version.size
|
return self.latest_version.size
|
||||||
|
|
||||||
|
# Compatibility methods
|
||||||
|
|
||||||
|
@property
|
||||||
|
def checksum(self):
|
||||||
|
return self.latest_version.checksum
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date_updated(self):
|
||||||
|
return self.latest_version.timestamp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_mime_encoding(self):
|
||||||
|
return self.latest_version.encoding
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_mimetype(self):
|
||||||
|
return self.latest_version.mimetype
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_version(self):
|
||||||
|
return self.versions.order_by('timestamp').last()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def page_count(self):
|
||||||
|
return self.latest_version.page_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pages_all(self):
|
||||||
|
try:
|
||||||
|
return self.latest_version.pages_all
|
||||||
|
except AttributeError:
|
||||||
|
# Document has no version yet
|
||||||
|
DocumentPage = apps.get_model(
|
||||||
|
app_label='documents', model_name='DocumentPage'
|
||||||
|
)
|
||||||
|
|
||||||
|
return DocumentPage.objects.none()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pages(self):
|
||||||
|
try:
|
||||||
|
return self.latest_version.pages
|
||||||
|
except AttributeError:
|
||||||
|
# Document has no version yet
|
||||||
|
DocumentPage = apps.get_model(
|
||||||
|
app_label='documents', model_name='DocumentPage'
|
||||||
|
)
|
||||||
|
|
||||||
|
return DocumentPage.objects.none()
|
||||||
|
|||||||
@@ -253,6 +253,10 @@ class DocumentVersion(models.Model):
|
|||||||
)
|
)
|
||||||
return DocumentPage.passthrough.filter(document_version=self)
|
return DocumentPage.passthrough.filter(document_version=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pages(self):
|
||||||
|
return self.version_pages.all()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def page_count(self):
|
def page_count(self):
|
||||||
"""
|
"""
|
||||||
@@ -260,10 +264,6 @@ class DocumentVersion(models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.pages.count()
|
return self.pages.count()
|
||||||
|
|
||||||
@property
|
|
||||||
def pages(self):
|
|
||||||
return self.version_pages.all()
|
|
||||||
|
|
||||||
def revert(self, _user=None):
|
def revert(self, _user=None):
|
||||||
"""
|
"""
|
||||||
Delete the subsequent versions after this one
|
Delete the subsequent versions after this one
|
||||||
|
|||||||
@@ -78,3 +78,7 @@ queue_uploads.add_task_type(
|
|||||||
dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_for',
|
dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_for',
|
||||||
label=_('Scan document duplicates')
|
label=_('Scan document duplicates')
|
||||||
)
|
)
|
||||||
|
queue_uploads.add_task_type(
|
||||||
|
dotted_path='mayan.apps.documents.tasks.task_upload_new_document',
|
||||||
|
label=_('Upload new document')
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ from django.db import OperationalError
|
|||||||
from mayan.celery import app
|
from mayan.celery import app
|
||||||
|
|
||||||
from .literals import (
|
from .literals import (
|
||||||
UPDATE_PAGE_COUNT_RETRY_DELAY, UPLOAD_NEW_VERSION_RETRY_DELAY
|
UPDATE_PAGE_COUNT_RETRY_DELAY, UPLOAD_NEW_DOCUMENT_RETRY_DELAY,
|
||||||
|
UPLOAD_NEW_VERSION_RETRY_DELAY
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -121,6 +122,60 @@ def task_update_page_count(self, version_id):
|
|||||||
raise self.retry(exc=exception)
|
raise self.retry(exc=exception)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(bind=True, default_retry_delay=UPLOAD_NEW_DOCUMENT_RETRY_DELAY, ignore_result=True)
|
||||||
|
def task_upload_new_document(self, document_type_id, shared_uploaded_file_id):
|
||||||
|
DocumentType = apps.get_model(
|
||||||
|
app_label='documents', model_name='DocumentType'
|
||||||
|
)
|
||||||
|
|
||||||
|
SharedUploadedFile = apps.get_model(
|
||||||
|
app_label='common', model_name='SharedUploadedFile'
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
document_type = DocumentType.objects.get(pk=document_type_id)
|
||||||
|
shared_file = SharedUploadedFile.objects.get(
|
||||||
|
pk=shared_uploaded_file_id
|
||||||
|
)
|
||||||
|
except OperationalError as exception:
|
||||||
|
logger.warning(
|
||||||
|
'Operational error during attempt to retrieve shared data for '
|
||||||
|
'new document of type: %s; %s. Retrying.', document_type, exception
|
||||||
|
)
|
||||||
|
raise self.retry(exc=exception)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with shared_file.open() as file_object:
|
||||||
|
document_type.new_document(file_object=file_object)
|
||||||
|
except OperationalError as exception:
|
||||||
|
logger.warning(
|
||||||
|
'Operational error during attempt to create new document '
|
||||||
|
'of type: %s; %s. Retrying.', document_type, exception
|
||||||
|
)
|
||||||
|
raise self.retry(exc=exception)
|
||||||
|
except Exception as exception:
|
||||||
|
# This except and else block emulate a finally:
|
||||||
|
logger.error(
|
||||||
|
'Unexpected error during attempt to create new document '
|
||||||
|
'of type: %s; %s', document_type, exception
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
shared_file.delete()
|
||||||
|
except OperationalError as exception:
|
||||||
|
logger.warning(
|
||||||
|
'Operational error during attempt to delete shared '
|
||||||
|
'file: %s; %s.', shared_file, exception
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
shared_file.delete()
|
||||||
|
except OperationalError as exception:
|
||||||
|
logger.warning(
|
||||||
|
'Operational error during attempt to delete shared '
|
||||||
|
'file: %s; %s.', shared_file, exception
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.task(bind=True, default_retry_delay=UPLOAD_NEW_VERSION_RETRY_DELAY, ignore_result=True)
|
@app.task(bind=True, default_retry_delay=UPLOAD_NEW_VERSION_RETRY_DELAY, ignore_result=True)
|
||||||
def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, comment=None):
|
def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, comment=None):
|
||||||
SharedUploadedFile = apps.get_model(
|
SharedUploadedFile = apps.get_model(
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import os
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from mayan.apps.converter.classes import Layer
|
from mayan.apps.converter.classes import Layer
|
||||||
from mayan.apps.converter.layers import layer_saved_transformations
|
|
||||||
|
|
||||||
from ..literals import PAGE_RANGE_ALL
|
from ..literals import PAGE_RANGE_ALL
|
||||||
from ..models import DocumentType
|
from ..models import DocumentType
|
||||||
@@ -15,7 +14,6 @@ from .literals import (
|
|||||||
TEST_DOCUMENT_TYPE_LABEL, TEST_DOCUMENT_TYPE_LABEL_EDITED,
|
TEST_DOCUMENT_TYPE_LABEL, TEST_DOCUMENT_TYPE_LABEL_EDITED,
|
||||||
TEST_DOCUMENT_TYPE_QUICK_LABEL, TEST_DOCUMENT_TYPE_QUICK_LABEL_EDITED,
|
TEST_DOCUMENT_TYPE_QUICK_LABEL, TEST_DOCUMENT_TYPE_QUICK_LABEL_EDITED,
|
||||||
TEST_SMALL_DOCUMENT_FILENAME, TEST_SMALL_DOCUMENT_PATH,
|
TEST_SMALL_DOCUMENT_FILENAME, TEST_SMALL_DOCUMENT_PATH,
|
||||||
TEST_TRANSFORMATION_ARGUMENT, TEST_TRANSFORMATION_CLASS,
|
|
||||||
TEST_VERSION_COMMENT
|
TEST_VERSION_COMMENT
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,8 +69,6 @@ class DocumentTestMixin(object):
|
|||||||
|
|
||||||
self.test_document = document
|
self.test_document = document
|
||||||
self.test_documents.append(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):
|
class DocumentTypeViewTestMixin(object):
|
||||||
@@ -153,13 +149,6 @@ class DocumentVersionTestMixin(object):
|
|||||||
|
|
||||||
|
|
||||||
class DocumentViewTestMixin(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):
|
def _request_document_properties_view(self):
|
||||||
return self.get(
|
return self.get(
|
||||||
viewname='documents:document_properties',
|
viewname='documents:document_properties',
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ from ..permissions import (
|
|||||||
from .base import GenericDocumentViewTestCase
|
from .base import GenericDocumentViewTestCase
|
||||||
|
|
||||||
|
|
||||||
class DocumentPageDisableViewTestMixin(object):
|
class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase):
|
||||||
def _disable_test_document_page(self):
|
def setUp(self):
|
||||||
self.test_document_page.enabled = False
|
super(DocumentPageDisableViewTestCase, self).setUp()
|
||||||
self.test_document_page.save()
|
self.test_document_page = self.test_document.pages_all.first()
|
||||||
|
|
||||||
def _request_test_document_page_disable_view(self):
|
def _request_test_document_page_disable_view(self):
|
||||||
return self.post(
|
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):
|
def test_document_page_disable_view_no_permission(self):
|
||||||
test_document_page_count = self.test_document.pages.count()
|
test_document_page_count = self.test_document.pages.count()
|
||||||
|
|
||||||
@@ -70,6 +45,13 @@ class DocumentPageDisableViewTestCase(
|
|||||||
test_document_page_count, self.test_document.pages.count()
|
test_document_page_count, self.test_document.pages.count()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _request_test_document_page_multiple_disable_view(self):
|
||||||
|
return self.post(
|
||||||
|
viewname='documents:document_page_multiple_disable', data={
|
||||||
|
'id_list': self.test_document_page.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_page_multiple_disable_view_no_permission(self):
|
def test_document_page_multiple_disable_view_no_permission(self):
|
||||||
test_document_page_count = self.test_document.pages.count()
|
test_document_page_count = self.test_document.pages.count()
|
||||||
|
|
||||||
@@ -94,6 +76,17 @@ class DocumentPageDisableViewTestCase(
|
|||||||
test_document_page_count, self.test_document.pages.count()
|
test_document_page_count, self.test_document.pages.count()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _disable_test_document_page(self):
|
||||||
|
self.test_document_page.enabled = False
|
||||||
|
self.test_document_page.save()
|
||||||
|
|
||||||
|
def _request_test_document_page_enable_view(self):
|
||||||
|
return self.post(
|
||||||
|
viewname='documents:document_page_enable', kwargs={
|
||||||
|
'pk': self.test_document_page.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_page_enable_view_no_permission(self):
|
def test_document_page_enable_view_no_permission(self):
|
||||||
self._disable_test_document_page()
|
self._disable_test_document_page()
|
||||||
|
|
||||||
@@ -121,6 +114,13 @@ class DocumentPageDisableViewTestCase(
|
|||||||
test_document_page_count, self.test_document.pages.count()
|
test_document_page_count, self.test_document.pages.count()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _request_test_document_page_multiple_enable_view(self):
|
||||||
|
return self.post(
|
||||||
|
viewname='documents:document_page_multiple_enable', data={
|
||||||
|
'id_list': self.test_document_page.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_page_multiple_enable_view_no_permission(self):
|
def test_document_page_multiple_enable_view_no_permission(self):
|
||||||
self._disable_test_document_page()
|
self._disable_test_document_page()
|
||||||
test_document_page_count = self.test_document.pages.count()
|
test_document_page_count = self.test_document.pages.count()
|
||||||
@@ -148,7 +148,7 @@ class DocumentPageDisableViewTestCase(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DocumentPageViewTestMixin(object):
|
class DocumentPageViewTestCase(GenericDocumentViewTestCase):
|
||||||
def _request_test_document_page_list_view(self):
|
def _request_test_document_page_list_view(self):
|
||||||
return self.get(
|
return self.get(
|
||||||
viewname='documents:document_pages', kwargs={
|
viewname='documents:document_pages', kwargs={
|
||||||
@@ -156,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):
|
def test_document_page_list_view_no_permission(self):
|
||||||
response = self._request_test_document_page_list_view()
|
response = self._request_test_document_page_list_view()
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
@@ -181,6 +170,13 @@ class DocumentPageViewTestCase(
|
|||||||
response=response, text=self.test_document.label, status_code=200
|
response=response, text=self.test_document.label, status_code=200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _request_test_document_page_view(self, document_page):
|
||||||
|
return self.get(
|
||||||
|
viewname='documents:document_page_view', kwargs={
|
||||||
|
'pk': document_page.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_page_view_no_permissions(self):
|
def test_document_page_view_no_permissions(self):
|
||||||
response = self._request_test_document_page_view(
|
response = self._request_test_document_page_view(
|
||||||
document_page=self.test_document.pages.first()
|
document_page=self.test_document.pages.first()
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class DocumentTypeQuickLabelViewsTestCase(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DocumentsQuickLabelViewTestMixin(object):
|
class DocumentsQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, GenericDocumentViewTestCase):
|
||||||
def _request_document_quick_label_edit_view(self, extra_data=None):
|
def _request_document_quick_label_edit_view(self, extra_data=None):
|
||||||
data = {
|
data = {
|
||||||
'document_type_available_filenames': self.test_document_type_filename.pk,
|
'document_type_available_filenames': self.test_document_type_filename.pk,
|
||||||
@@ -219,11 +219,6 @@ class DocumentsQuickLabelViewTestMixin(object):
|
|||||||
}, data=data
|
}, data=data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DocumentsQuickLabelViewTestCase(
|
|
||||||
DocumentsQuickLabelViewTestMixin, DocumentTypeQuickLabelTestMixin,
|
|
||||||
GenericDocumentViewTestCase
|
|
||||||
):
|
|
||||||
def test_document_quick_label_no_permission(self):
|
def test_document_quick_label_no_permission(self):
|
||||||
self._create_test_quick_label()
|
self._create_test_quick_label()
|
||||||
|
|
||||||
|
|||||||
@@ -9,24 +9,13 @@ from .literals import TEST_VERSION_COMMENT
|
|||||||
from .mixins import DocumentVersionTestMixin
|
from .mixins import DocumentVersionTestMixin
|
||||||
|
|
||||||
|
|
||||||
class DocumentVersionViewTestMixin(object):
|
class DocumentVersionTestCase(DocumentVersionTestMixin, GenericDocumentViewTestCase):
|
||||||
def _request_document_version_list_view(self):
|
def _request_document_version_list_view(self):
|
||||||
return self.get(
|
return self.get(
|
||||||
viewname='documents:document_version_list',
|
viewname='documents:document_version_list',
|
||||||
kwargs={'pk': self.test_document.pk}
|
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):
|
def test_document_version_list_no_permission(self):
|
||||||
self._upload_new_version()
|
self._upload_new_version()
|
||||||
|
|
||||||
@@ -44,6 +33,12 @@ class DocumentVersionViewTestCase(
|
|||||||
response=response, text=TEST_VERSION_COMMENT, status_code=200
|
response=response, text=TEST_VERSION_COMMENT, status_code=200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _request_document_version_revert_view(self, document_version):
|
||||||
|
return self.post(
|
||||||
|
viewname='documents:document_version_revert',
|
||||||
|
kwargs={'pk': document_version.pk}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_version_revert_no_permission(self):
|
def test_document_version_revert_no_permission(self):
|
||||||
first_version = self.test_document.latest_version
|
first_version = self.test_document.latest_version
|
||||||
self._upload_new_version()
|
self._upload_new_version()
|
||||||
|
|||||||
@@ -16,14 +16,22 @@ from ..permissions import (
|
|||||||
|
|
||||||
from .base import GenericDocumentViewTestCase
|
from .base import GenericDocumentViewTestCase
|
||||||
from .literals import (
|
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
|
from .mixins import DocumentViewTestMixin
|
||||||
|
|
||||||
|
|
||||||
class DocumentViewTestCase(
|
class DocumentsViewsTestCase(
|
||||||
LayerTestMixin, DocumentViewTestMixin, GenericDocumentViewTestCase
|
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):
|
def test_document_view_no_permissions(self):
|
||||||
response = self._request_document_properties_view()
|
response = self._request_document_properties_view()
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
@@ -294,13 +302,13 @@ class DocumentViewTestCase(
|
|||||||
self.assertEqual(self.test_document.pages.count(), 0)
|
self.assertEqual(self.test_document.pages.count(), 0)
|
||||||
|
|
||||||
def test_document_update_page_count_view_with_permission(self):
|
def test_document_update_page_count_view_with_permission(self):
|
||||||
|
# TODO: Revise permission association
|
||||||
|
|
||||||
page_count = self.test_document.pages.count()
|
page_count = self.test_document.pages.count()
|
||||||
self.test_document.pages.all().delete()
|
self.test_document.pages.all().delete()
|
||||||
self.assertEqual(self.test_document.pages.count(), 0)
|
self.assertEqual(self.test_document.pages.count(), 0)
|
||||||
|
|
||||||
self.grant_access(
|
self.grant_permission(permission=permission_document_tools)
|
||||||
obj=self.test_document, permission=permission_document_tools
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self._request_document_update_page_count_view()
|
response = self._request_document_update_page_count_view()
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
@@ -321,9 +329,7 @@ class DocumentViewTestCase(
|
|||||||
self.test_document.pages.all().delete()
|
self.test_document.pages.all().delete()
|
||||||
self.assertEqual(self.test_document.pages.count(), 0)
|
self.assertEqual(self.test_document.pages.count(), 0)
|
||||||
|
|
||||||
self.grant_access(
|
self.grant_permission(permission=permission_document_tools)
|
||||||
obj=self.test_document, permission=permission_document_tools
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self._request_document_multiple_update_page_count_view()
|
response = self._request_document_multiple_update_page_count_view()
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from ..permissions import permission_document_view
|
|||||||
from .base import GenericDocumentViewTestCase
|
from .base import GenericDocumentViewTestCase
|
||||||
|
|
||||||
|
|
||||||
class DuplicatedDocumentsViewsTestMixin(object):
|
class DuplicatedDocumentsViewsTestCase(GenericDocumentViewTestCase):
|
||||||
def _upload_duplicate_document(self):
|
def _upload_duplicate_document(self):
|
||||||
self.upload_document()
|
self.upload_document()
|
||||||
|
|
||||||
@@ -18,10 +18,6 @@ class DuplicatedDocumentsViewsTestMixin(object):
|
|||||||
kwargs={'pk': self.test_documents[0].pk}
|
kwargs={'pk': self.test_documents[0].pk}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DuplicatedDocumentsViewsTestCase(
|
|
||||||
DuplicatedDocumentsViewsTestMixin, GenericDocumentViewTestCase
|
|
||||||
):
|
|
||||||
def test_duplicated_document_list_no_permissions(self):
|
def test_duplicated_document_list_no_permissions(self):
|
||||||
self._upload_duplicate_document()
|
self._upload_duplicate_document()
|
||||||
|
|
||||||
|
|||||||
@@ -17,23 +17,12 @@ TEST_TRANSFORMATION_NAME = 'rotate'
|
|||||||
TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180'
|
TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180'
|
||||||
|
|
||||||
|
|
||||||
class DocumentEventsTestMixin(object):
|
class DocumentEventsTestCase(GenericDocumentViewTestCase):
|
||||||
def _request_test_document_download_view(self):
|
def _request_test_document_download_view(self):
|
||||||
return self.get(
|
return self.get(
|
||||||
'documents:document_download', kwargs={'pk': self.test_document.pk}
|
'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):
|
def test_document_download_event_no_permissions(self):
|
||||||
Action.objects.all().delete()
|
Action.objects.all().delete()
|
||||||
|
|
||||||
@@ -66,6 +55,13 @@ class DocumentEventsTestCase(
|
|||||||
self.assertEqual(event.target, self.test_document)
|
self.assertEqual(event.target, self.test_document)
|
||||||
self.assertEqual(event.verb, event_document_download.id)
|
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):
|
def test_document_view_event_no_permissions(self):
|
||||||
Action.objects.all().delete()
|
Action.objects.all().delete()
|
||||||
|
|
||||||
|
|||||||
@@ -6,34 +6,13 @@ from ..permissions import permission_document_view
|
|||||||
from .base import GenericDocumentViewTestCase
|
from .base import GenericDocumentViewTestCase
|
||||||
|
|
||||||
|
|
||||||
class FavoriteDocumentsTestMixin(object):
|
class FavoriteDocumentsTestCase(GenericDocumentViewTestCase):
|
||||||
def _request_document_add_to_favorites_view(self):
|
def _request_document_add_to_favorites_view(self):
|
||||||
return self.post(
|
return self.post(
|
||||||
viewname='documents:document_add_to_favorites',
|
viewname='documents:document_add_to_favorites',
|
||||||
kwargs={'pk': self.test_document.pk}
|
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):
|
def test_document_add_to_favorites_view_no_permission(self):
|
||||||
response = self._request_document_add_to_favorites_view()
|
response = self._request_document_add_to_favorites_view()
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
@@ -47,6 +26,16 @@ class FavoriteDocumentsTestCase(
|
|||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(FavoriteDocument.objects.count(), 1)
|
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):
|
def test_document_list_favorites_view_no_permission(self):
|
||||||
self._document_add_to_favorites()
|
self._document_add_to_favorites()
|
||||||
response = self._request_document_list_favorites()
|
response = self._request_document_list_favorites()
|
||||||
@@ -64,6 +53,12 @@ class FavoriteDocumentsTestCase(
|
|||||||
response=response, text=self.test_document.label, status_code=200
|
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):
|
def test_document_remove_from_favorites_view_no_permission(self):
|
||||||
self._document_add_to_favorites()
|
self._document_add_to_favorites()
|
||||||
response = self._request_document_remove_from_favorites()
|
response = self._request_document_remove_from_favorites()
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class DocumentsLinksTestCase(GenericDocumentViewTestCase):
|
|||||||
class DeletedDocumentsLinksTestCase(GenericDocumentViewTestCase):
|
class DeletedDocumentsLinksTestCase(GenericDocumentViewTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(DeletedDocumentsLinksTestCase, self).setUp()
|
super(DeletedDocumentsLinksTestCase, self).setUp()
|
||||||
|
self.login_user()
|
||||||
self.test_document.delete()
|
self.test_document.delete()
|
||||||
self.test_deleted_document = DeletedDocument.objects.get(
|
self.test_deleted_document = DeletedDocument.objects.get(
|
||||||
pk=self.test_document.pk
|
pk=self.test_document.pk
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
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.permissions import permission_document_view
|
||||||
from mayan.apps.documents.search import document_search, document_page_search
|
from mayan.apps.documents.search import document_search, document_page_search
|
||||||
|
from mayan.apps.documents.tests.mixins import DocumentTestMixin
|
||||||
from .base import GenericDocumentViewTestCase
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentSearchTestMixin(object):
|
class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase):
|
||||||
def _perform_document_page_search(self):
|
def _perform_document_page_search(self):
|
||||||
return document_page_search.search(
|
return document_page_search.search(
|
||||||
query_string={'q': self.test_document.label}, user=self._test_case_user
|
query_string={'q': self.test_document.label}, user=self._test_case_user
|
||||||
@@ -17,10 +17,6 @@ class DocumentSearchTestMixin(object):
|
|||||||
query_string={'q': self.test_document.label}, user=self._test_case_user
|
query_string={'q': self.test_document.label}, user=self._test_case_user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DocumentSearchTestCase(
|
|
||||||
DocumentSearchTestMixin, GenericDocumentViewTestCase
|
|
||||||
):
|
|
||||||
def test_document_page_search_no_access(self):
|
def test_document_page_search_no_access(self):
|
||||||
queryset = self._perform_document_page_search()
|
queryset = self._perform_document_page_search()
|
||||||
self.assertFalse(self.test_document.pages.first() in queryset)
|
self.assertFalse(self.test_document.pages.first() in queryset)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from ..permissions import (
|
|||||||
from .base import GenericDocumentViewTestCase
|
from .base import GenericDocumentViewTestCase
|
||||||
|
|
||||||
|
|
||||||
class TrashedDocumentTestMixin(object):
|
class TrashedDocumentTestCase(GenericDocumentViewTestCase):
|
||||||
def _request_document_restore_get_view(self):
|
def _request_document_restore_get_view(self):
|
||||||
return self.get(
|
return self.get(
|
||||||
viewname='documents:document_restore', kwargs={
|
viewname='documents:document_restore', kwargs={
|
||||||
@@ -17,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):
|
def test_document_restore_get_view_no_permission(self):
|
||||||
self.test_document.delete()
|
self.test_document.delete()
|
||||||
self.assertEqual(Document.objects.count(), 0)
|
self.assertEqual(Document.objects.count(), 0)
|
||||||
@@ -86,6 +43,13 @@ class TrashedDocumentTestCase(
|
|||||||
|
|
||||||
self.assertEqual(Document.objects.count(), document_count)
|
self.assertEqual(Document.objects.count(), document_count)
|
||||||
|
|
||||||
|
def _request_document_restore_post_view(self):
|
||||||
|
return self.post(
|
||||||
|
viewname='documents:document_restore', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_restore_post_view_no_permission(self):
|
def test_document_restore_post_view_no_permission(self):
|
||||||
self.test_document.delete()
|
self.test_document.delete()
|
||||||
self.assertEqual(Document.objects.count(), 0)
|
self.assertEqual(Document.objects.count(), 0)
|
||||||
@@ -110,6 +74,13 @@ class TrashedDocumentTestCase(
|
|||||||
self.assertEqual(DeletedDocument.objects.count(), 0)
|
self.assertEqual(DeletedDocument.objects.count(), 0)
|
||||||
self.assertEqual(Document.objects.count(), 1)
|
self.assertEqual(Document.objects.count(), 1)
|
||||||
|
|
||||||
|
def _request_document_trash_get_view(self):
|
||||||
|
return self.get(
|
||||||
|
viewname='documents:document_trash', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_trash_get_view_no_permissions(self):
|
def test_document_trash_get_view_no_permissions(self):
|
||||||
document_count = Document.objects.count()
|
document_count = Document.objects.count()
|
||||||
|
|
||||||
@@ -130,6 +101,13 @@ class TrashedDocumentTestCase(
|
|||||||
|
|
||||||
self.assertEqual(Document.objects.count(), document_count)
|
self.assertEqual(Document.objects.count(), document_count)
|
||||||
|
|
||||||
|
def _request_document_trash_post_view(self):
|
||||||
|
return self.post(
|
||||||
|
viewname='documents:document_trash', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_trash_post_view_no_permissions(self):
|
def test_document_trash_post_view_no_permissions(self):
|
||||||
response = self._request_document_trash_post_view()
|
response = self._request_document_trash_post_view()
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
@@ -148,6 +126,13 @@ class TrashedDocumentTestCase(
|
|||||||
self.assertEqual(DeletedDocument.objects.count(), 1)
|
self.assertEqual(DeletedDocument.objects.count(), 1)
|
||||||
self.assertEqual(Document.objects.count(), 0)
|
self.assertEqual(Document.objects.count(), 0)
|
||||||
|
|
||||||
|
def _request_document_delete_get_view(self):
|
||||||
|
return self.get(
|
||||||
|
viewname='documents:document_delete', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_delete_get_view_no_permissions(self):
|
def test_document_delete_get_view_no_permissions(self):
|
||||||
self.test_document.delete()
|
self.test_document.delete()
|
||||||
self.assertEqual(Document.objects.count(), 0)
|
self.assertEqual(Document.objects.count(), 0)
|
||||||
@@ -180,6 +165,13 @@ class TrashedDocumentTestCase(
|
|||||||
DeletedDocument.objects.count(), trashed_document_count
|
DeletedDocument.objects.count(), trashed_document_count
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _request_document_delete_post_view(self):
|
||||||
|
return self.post(
|
||||||
|
viewname='documents:document_delete', kwargs={
|
||||||
|
'pk': self.test_document.pk
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_document_delete_post_view_no_permissions(self):
|
def test_document_delete_post_view_no_permissions(self):
|
||||||
self.test_document.delete()
|
self.test_document.delete()
|
||||||
self.assertEqual(Document.objects.count(), 0)
|
self.assertEqual(Document.objects.count(), 0)
|
||||||
@@ -206,6 +198,9 @@ class TrashedDocumentTestCase(
|
|||||||
self.assertEqual(DeletedDocument.objects.count(), 0)
|
self.assertEqual(DeletedDocument.objects.count(), 0)
|
||||||
self.assertEqual(Document.objects.count(), 0)
|
self.assertEqual(Document.objects.count(), 0)
|
||||||
|
|
||||||
|
def _request_document_list_deleted_view(self):
|
||||||
|
return self.get(viewname='documents:document_list_deleted')
|
||||||
|
|
||||||
def test_deleted_document_list_view_no_permissions(self):
|
def test_deleted_document_list_view_no_permissions(self):
|
||||||
self.test_document.delete()
|
self.test_document.delete()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.db.models.signals import post_migrate
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from mayan.apps.common.apps import MayanAppConfig
|
from mayan.apps.common.apps import MayanAppConfig
|
||||||
@@ -11,6 +12,7 @@ from mayan.apps.common.menus import (
|
|||||||
from mayan.apps.navigation.classes import SourceColumn
|
from mayan.apps.navigation.classes import SourceColumn
|
||||||
|
|
||||||
from .dependencies import * # NOQA
|
from .dependencies import * # NOQA
|
||||||
|
from .handlers import handler_create_system_user
|
||||||
from .html_widgets import (
|
from .html_widgets import (
|
||||||
ObjectLinkWidget, widget_event_actor_link, widget_event_type_link
|
ObjectLinkWidget, widget_event_actor_link, widget_event_type_link
|
||||||
)
|
)
|
||||||
@@ -101,3 +103,8 @@ class EventsApp(MayanAppConfig):
|
|||||||
link_event_types_subscriptions_list, link_current_user_events
|
link_event_types_subscriptions_list, link_current_user_events
|
||||||
), position=50
|
), position=50
|
||||||
)
|
)
|
||||||
|
|
||||||
|
post_migrate.connect(
|
||||||
|
dispatch_uid='events_create_system_user',
|
||||||
|
receiver=handler_create_system_user,
|
||||||
|
)
|
||||||
|
|||||||
7
mayan/apps/events/handlers.py
Normal file
7
mayan/apps/events/handlers.py
Normal 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()
|
||||||
23
mayan/apps/events/utils.py
Normal file
23
mayan/apps/events/utils.py
Normal 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
|
||||||
3
mayan/apps/importer/__init__.py
Normal file
3
mayan/apps/importer/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
default_app_config = 'mayan.apps.importer.apps.ImporterApp'
|
||||||
17
mayan/apps/importer/apps.py
Normal file
17
mayan/apps/importer/apps.py
Normal 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()
|
||||||
0
mayan/apps/importer/management/__init__.py
Normal file
0
mayan/apps/importer/management/__init__.py
Normal file
0
mayan/apps/importer/management/commands/__init__.py
Normal file
0
mayan/apps/importer/management/commands/__init__.py
Normal file
152
mayan/apps/importer/management/commands/import.py
Normal file
152
mayan/apps/importer/management/commands/import.py
Normal 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)
|
||||||
|
)
|
||||||
10
mayan/apps/importer/queues.py
Normal file
10
mayan/apps/importer/queues.py
Normal 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')
|
||||||
|
)
|
||||||
93
mayan/apps/importer/tasks.py
Normal file
93
mayan/apps/importer/tasks.py
Normal 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
|
||||||
|
)
|
||||||
0
mayan/apps/importer/tests/__init__.py
Normal file
0
mayan/apps/importer/tests/__init__.py
Normal file
117
mayan/apps/importer/tests/test_management_commands.py
Normal file
117
mayan/apps/importer/tests/test_management_commands.py
Normal 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'
|
||||||
|
)
|
||||||
@@ -34,7 +34,7 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT
|
|||||||
|
|
||||||
def test_upload_interactive_with_unicode_metadata(self):
|
def test_upload_interactive_with_unicode_metadata(self):
|
||||||
url = URL(
|
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_id'] = self.test_metadata_type.pk
|
||||||
url.args['metadata0_value'] = TEST_METADATA_VALUE_UNICODE
|
url.args['metadata0_value'] = TEST_METADATA_VALUE_UNICODE
|
||||||
@@ -61,7 +61,7 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT
|
|||||||
|
|
||||||
def test_upload_interactive_with_ampersand_metadata(self):
|
def test_upload_interactive_with_ampersand_metadata(self):
|
||||||
url = URL(
|
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_id'] = self.test_metadata_type.pk
|
||||||
url.args['metadata0_value'] = TEST_METADATA_VALUE_WITH_AMPERSAND
|
url.args['metadata0_value'] = TEST_METADATA_VALUE_WITH_AMPERSAND
|
||||||
|
|||||||
@@ -689,7 +689,7 @@ class SourceColumn(object):
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
'No request variable, aborting request resolution'
|
'No request variable, aborting request resolution'
|
||||||
)
|
)
|
||||||
return result
|
return final_result
|
||||||
|
|
||||||
current_view_name = get_current_view_name(request=request)
|
current_view_name = get_current_view_name(request=request)
|
||||||
for column in columns:
|
for column in columns:
|
||||||
|
|||||||
@@ -108,12 +108,6 @@ def navigation_resolve_menus(context, names, source=None, sort_results=None):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
|
||||||
def resolve_link(context, link):
|
|
||||||
# This can be used to resolve links or menus too
|
|
||||||
return link.resolve(context=context)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def navigation_source_column_resolve(context, column):
|
def navigation_source_column_resolve(context, column):
|
||||||
if column:
|
if column:
|
||||||
@@ -121,3 +115,9 @@ def navigation_source_column_resolve(context, column):
|
|||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def resolve_link(context, link):
|
||||||
|
# This can be used to resolve links or menus too
|
||||||
|
return link.resolve(context=context)
|
||||||
|
|||||||
11
mayan/apps/redactions/icons.py
Normal file
11
mayan/apps/redactions/icons.py
Normal 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')
|
||||||
@@ -145,9 +145,8 @@ class SourcesApp(MayanAppConfig):
|
|||||||
menu_secondary.bind_links(
|
menu_secondary.bind_links(
|
||||||
links=(link_document_version_upload,),
|
links=(link_document_version_upload,),
|
||||||
sources=(
|
sources=(
|
||||||
'documents:document_version_list',
|
'documents:document_version_list', 'documents:upload_version',
|
||||||
'documents:document_version_revert',
|
'documents:document_version_revert'
|
||||||
'sources:document_version_upload'
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,14 @@ class NewDocumentForm(DocumentForm):
|
|||||||
|
|
||||||
|
|
||||||
class NewVersionForm(forms.Form):
|
class NewVersionForm(forms.Form):
|
||||||
comment = forms.CharField(
|
def __init__(self, *args, **kwargs):
|
||||||
help_text=_('An optional comment to explain the upload.'),
|
super(NewVersionForm, self).__init__(*args, **kwargs)
|
||||||
label=_('Comment'), required=False,
|
|
||||||
widget=forms.widgets.Textarea(attrs={'rows': 4}),
|
self.fields['comment'] = forms.CharField(
|
||||||
)
|
label=_('Comment'),
|
||||||
|
required=False,
|
||||||
|
widget=forms.widgets.Textarea(attrs={'rows': 4}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UploadBaseForm(forms.Form):
|
class UploadBaseForm(forms.Form):
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ link_document_version_upload = Link(
|
|||||||
args='resolved_object.pk', condition=document_new_version_not_blocked,
|
args='resolved_object.pk', condition=document_new_version_not_blocked,
|
||||||
icon_class_path='mayan.apps.sources.icons.icon_document_version_upload',
|
icon_class_path='mayan.apps.sources.icons.icon_document_version_upload',
|
||||||
permissions=(permission_document_new_version,),
|
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(
|
link_setup_source_logs = Link(
|
||||||
args=('resolved_object.pk',),
|
args=('resolved_object.pk',),
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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 '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>
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,6 @@ from .literals import TEST_SOURCE_LABEL, TEST_SOURCE_UNCOMPRESS_N
|
|||||||
|
|
||||||
|
|
||||||
class SourceTestMixin(object):
|
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):
|
def _create_test_source(self):
|
||||||
self.test_source = WebFormSource.objects.create(
|
self.test_source = WebFormSource.objects.create(
|
||||||
enabled=True, label=TEST_SOURCE_LABEL,
|
enabled=True, label=TEST_SOURCE_LABEL,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class CompressedUploadsTestCase(SourceTestMixin, GenericDocumentTestCase):
|
|||||||
auto_upload_document = False
|
auto_upload_document = False
|
||||||
|
|
||||||
def test_upload_compressed_file(self):
|
def test_upload_compressed_file(self):
|
||||||
|
self._create_test_source()
|
||||||
self.test_source.uncompress = SOURCE_UNCOMPRESS_CHOICE_Y
|
self.test_source.uncompress = SOURCE_UNCOMPRESS_CHOICE_Y
|
||||||
self.test_source.save()
|
self.test_source.save()
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class DocumentUploadWizardViewTestMixin(object):
|
|||||||
def _request_upload_wizard_view(self, document_path=TEST_SMALL_DOCUMENT_PATH):
|
def _request_upload_wizard_view(self, document_path=TEST_SMALL_DOCUMENT_PATH):
|
||||||
with open(document_path, mode='rb') as file_object:
|
with open(document_path, mode='rb') as file_object:
|
||||||
return self.post(
|
return self.post(
|
||||||
viewname='sources:document_upload_interactive', kwargs={
|
viewname='sources:upload_interactive', kwargs={
|
||||||
'source_id': self.test_source.pk
|
'source_id': self.test_source.pk
|
||||||
}, data={
|
}, data={
|
||||||
'source-file': file_object,
|
'source-file': file_object,
|
||||||
@@ -42,7 +42,7 @@ class DocumentUploadWizardViewTestMixin(object):
|
|||||||
|
|
||||||
def _request_upload_interactive_view(self):
|
def _request_upload_interactive_view(self):
|
||||||
return self.get(
|
return self.get(
|
||||||
viewname='sources:document_upload_interactive', data={
|
viewname='sources:upload_interactive', data={
|
||||||
'document_type_id': self.test_document_type.pk,
|
'document_type_id': self.test_document_type.pk,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -54,6 +54,10 @@ class DocumentUploadWizardViewTestCase(
|
|||||||
):
|
):
|
||||||
auto_upload_document = False
|
auto_upload_document = False
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DocumentUploadWizardViewTestCase, self).setUp()
|
||||||
|
self._create_test_source()
|
||||||
|
|
||||||
def test_upload_compressed_file(self):
|
def test_upload_compressed_file(self):
|
||||||
self.test_source.uncompress = SOURCE_UNCOMPRESS_CHOICE_Y
|
self.test_source.uncompress = SOURCE_UNCOMPRESS_CHOICE_Y
|
||||||
self.test_source.save()
|
self.test_source.save()
|
||||||
@@ -109,7 +113,7 @@ class DocumentUploadWizardViewTestCase(
|
|||||||
|
|
||||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||||
response = self.post(
|
response = self.post(
|
||||||
viewname='sources:document_upload_interactive', kwargs={
|
viewname='sources:upload_interactive', kwargs={
|
||||||
'source_id': self.test_source.pk
|
'source_id': self.test_source.pk
|
||||||
}, data={
|
}, data={
|
||||||
'source-file': file_object,
|
'source-file': file_object,
|
||||||
@@ -153,7 +157,7 @@ class DocumentUploadIssueTestCase(GenericDocumentViewTestCase):
|
|||||||
# Upload the test document
|
# Upload the test document
|
||||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||||
self.post(
|
self.post(
|
||||||
viewname='sources:document_upload_interactive', data={
|
viewname='sources:upload_interactive', data={
|
||||||
'document-language': 'eng',
|
'document-language': 'eng',
|
||||||
'source-file': file_object,
|
'source-file': file_object,
|
||||||
'document_type_id': self.test_document_type.pk
|
'document_type_id': self.test_document_type.pk
|
||||||
@@ -203,7 +207,7 @@ class NewDocumentVersionViewTestCase(GenericDocumentViewTestCase):
|
|||||||
NewVersionBlock.objects.block(self.test_document)
|
NewVersionBlock.objects.block(self.test_document)
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
viewname='sources:document_version_upload', kwargs={
|
viewname='sources:upload_version', kwargs={
|
||||||
'document_pk': self.test_document.pk
|
'document_pk': self.test_document.pk
|
||||||
}, follow=True
|
}, follow=True
|
||||||
)
|
)
|
||||||
@@ -296,8 +300,6 @@ class StagingFolderViewTestCase(
|
|||||||
class SourcesViewTestCase(
|
class SourcesViewTestCase(
|
||||||
SourceTestMixin, SourceViewTestMixin, GenericViewTestCase
|
SourceTestMixin, SourceViewTestMixin, GenericViewTestCase
|
||||||
):
|
):
|
||||||
auto_create_test_source = False
|
|
||||||
|
|
||||||
def test_source_create_view_no_permission(self):
|
def test_source_create_view_no_permission(self):
|
||||||
response = self._request_setup_source_create_view()
|
response = self._request_setup_source_create_view()
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from .api_views import (
|
|||||||
from .views import (
|
from .views import (
|
||||||
SetupSourceCheckView, SetupSourceCreateView, SetupSourceDeleteView,
|
SetupSourceCheckView, SetupSourceCreateView, SetupSourceDeleteView,
|
||||||
SetupSourceEditView, SetupSourceListView, SourceLogListView,
|
SetupSourceEditView, SetupSourceListView, SourceLogListView,
|
||||||
StagingFileDeleteView, DocumentVersionUploadInteractiveView, UploadInteractiveView
|
StagingFileDeleteView, UploadInteractiveVersionView, UploadInteractiveView
|
||||||
)
|
)
|
||||||
from .wizards import DocumentCreateWizard
|
from .wizards import DocumentCreateWizard
|
||||||
|
|
||||||
@@ -27,23 +27,19 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/upload/new/interactive/(?P<source_id>\d+)/$',
|
regex=r'^documents/upload/new/interactive/(?P<source_id>\d+)/$',
|
||||||
view=UploadInteractiveView.as_view(),
|
view=UploadInteractiveView.as_view(), name='upload_interactive'
|
||||||
name='document_upload_interactive'
|
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/upload/new/interactive/$',
|
regex=r'^documents/upload/new/interactive/$',
|
||||||
view=UploadInteractiveView.as_view(),
|
view=UploadInteractiveView.as_view(), name='upload_interactive'
|
||||||
name='document_upload_interactive'
|
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/(?P<document_pk>\d+)/versions/upload/interactive/(?P<source_id>\d+)/$',
|
regex=r'^documents/(?P<document_pk>\d+)/versions/upload/interactive/(?P<source_id>\d+)/$',
|
||||||
view=DocumentVersionUploadInteractiveView.as_view(),
|
view=UploadInteractiveVersionView.as_view(), name='upload_version'
|
||||||
name='document_version_upload'
|
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
regex=r'^documents/(?P<document_pk>\d+)/versions/upload/interactive/$',
|
regex=r'^documents/(?P<document_pk>\d+)/versions/upload/interactive/$',
|
||||||
view=DocumentVersionUploadInteractiveView.as_view(),
|
view=UploadInteractiveVersionView.as_view(), name='upload_version'
|
||||||
name='document_version_upload'
|
|
||||||
),
|
),
|
||||||
|
|
||||||
# Setup views
|
# Setup views
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ class UploadBaseView(MultiFormView):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_tab_link_for_source(source, document=None):
|
def get_tab_link_for_source(source, document=None):
|
||||||
if document:
|
if document:
|
||||||
view = 'sources:document_version_upload'
|
view = 'sources:upload_version'
|
||||||
args = ('"{}"'.format(document.pk), '"{}"'.format(source.pk),)
|
args = ('"{}"'.format(document.pk), '"{}"'.format(source.pk),)
|
||||||
else:
|
else:
|
||||||
view = 'sources:document_upload_interactive'
|
view = 'sources:upload_interactive'
|
||||||
args = ('"{}"'.format(source.pk),)
|
args = ('"{}"'.format(source.pk),)
|
||||||
|
|
||||||
return Link(
|
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:upload_interactive'] = self.tab_links
|
||||||
menu_facet.bound_links['sources:document_version_upload'] = self.tab_links
|
menu_facet.bound_links['sources:upload_version'] = self.tab_links
|
||||||
|
|
||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
@@ -360,7 +360,7 @@ class UploadInteractiveView(UploadBaseView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class DocumentVersionUploadInteractiveView(UploadBaseView):
|
class UploadInteractiveVersionView(UploadBaseView):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
|
||||||
self.subtemplates_list = []
|
self.subtemplates_list = []
|
||||||
@@ -392,7 +392,7 @@ class DocumentVersionUploadInteractiveView(UploadBaseView):
|
|||||||
self.tab_links = UploadBaseView.get_active_tab_links(self.document)
|
self.tab_links = UploadBaseView.get_active_tab_links(self.document)
|
||||||
|
|
||||||
return super(
|
return super(
|
||||||
DocumentVersionUploadInteractiveView, self
|
UploadInteractiveVersionView, self
|
||||||
).dispatch(request, *args, **kwargs)
|
).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def forms_valid(self, forms):
|
def forms_valid(self, forms):
|
||||||
@@ -448,36 +448,27 @@ class DocumentVersionUploadInteractiveView(UploadBaseView):
|
|||||||
files=kwargs.get('files', None),
|
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):
|
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 {
|
return {
|
||||||
'document_form': NewVersionForm,
|
'document_form': NewVersionForm,
|
||||||
'source_form': source_form_class
|
'source_form': get_upload_form_class(self.source.source_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(
|
context = super(
|
||||||
DocumentVersionUploadInteractiveView, self
|
UploadInteractiveVersionView, self
|
||||||
).get_context_data(**kwargs)
|
).get_context_data(**kwargs)
|
||||||
context['object'] = self.document
|
context['object'] = self.document
|
||||||
context['title'] = _(
|
context['title'] = _(
|
||||||
'Upload a new version for document "%(document)s" '
|
'Upload a new version from source: %s'
|
||||||
'from source: %(source)s'
|
) % self.source.label
|
||||||
) % {'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']
|
|
||||||
)
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ class DocumentCreateWizard(SessionWizardView):
|
|||||||
for step in WizardStep.get_all():
|
for step in WizardStep.get_all():
|
||||||
query_dict.update(step.done(wizard=self) or {})
|
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
|
# Use equal and not .update() to get the same result as using
|
||||||
# urlencode(doseq=True)
|
# urlencode(doseq=True)
|
||||||
url.args = query_dict
|
url.args = query_dict
|
||||||
|
|||||||
Binary file not shown.
@@ -27,7 +27,7 @@ class TaggedDocumentUploadTestCase(TagTestMixin, GenericDocumentViewTestCase):
|
|||||||
def _request_upload_interactive_document_create_view(self):
|
def _request_upload_interactive_document_create_view(self):
|
||||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||||
return self.post(
|
return self.post(
|
||||||
viewname='sources:document_upload_interactive', kwargs={
|
viewname='sources:upload_interactive', kwargs={
|
||||||
'source_id': self.test_source.pk
|
'source_id': self.test_source.pk
|
||||||
}, data={
|
}, data={
|
||||||
'document_type_id': self.test_document_type.pk,
|
'document_type_id': self.test_document_type.pk,
|
||||||
|
|||||||
3
mayan/apps/weblinks/__init__.py
Normal file
3
mayan/apps/weblinks/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
default_app_config = 'mayan.apps.weblinks.apps.WebLinksApp'
|
||||||
15
mayan/apps/weblinks/admin.py
Normal file
15
mayan/apps/weblinks/admin.py
Normal 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
123
mayan/apps/weblinks/apps.py
Normal 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)
|
||||||
16
mayan/apps/weblinks/events.py
Normal file
16
mayan/apps/weblinks/events.py
Normal 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'
|
||||||
|
)
|
||||||
11
mayan/apps/weblinks/forms.py
Normal file
11
mayan/apps/weblinks/forms.py
Normal 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
|
||||||
22
mayan/apps/weblinks/icons.py
Normal file
22
mayan/apps/weblinks/icons.py
Normal 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
|
||||||
63
mayan/apps/weblinks/links.py
Normal file
63
mayan/apps/weblinks/links.py
Normal 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'
|
||||||
|
)
|
||||||
16
mayan/apps/weblinks/managers.py
Normal file
16
mayan/apps/weblinks/managers.py
Normal 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
|
||||||
|
)
|
||||||
42
mayan/apps/weblinks/migrations/0001_initial.py
Normal file
42
mayan/apps/weblinks/migrations/0001_initial.py
Normal 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',),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
mayan/apps/weblinks/migrations/__init__.py
Normal file
0
mayan/apps/weblinks/migrations/__init__.py
Normal file
90
mayan/apps/weblinks/models.py
Normal file
90
mayan/apps/weblinks/models.py
Normal 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)
|
||||||
23
mayan/apps/weblinks/permissions.py
Normal file
23
mayan/apps/weblinks/permissions.py
Normal 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'
|
||||||
|
)
|
||||||
47
mayan/apps/weblinks/urls.py
Normal file
47
mayan/apps/weblinks/urls.py
Normal 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'
|
||||||
|
),
|
||||||
|
]
|
||||||
233
mayan/apps/weblinks/views.py
Normal file
233
mayan/apps/weblinks/views.py
Normal 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}
|
||||||
@@ -117,6 +117,7 @@ INSTALLED_APPS = (
|
|||||||
'mayan.apps.document_states',
|
'mayan.apps.document_states',
|
||||||
'mayan.apps.documents',
|
'mayan.apps.documents',
|
||||||
'mayan.apps.file_metadata',
|
'mayan.apps.file_metadata',
|
||||||
|
'mayan.apps.importer',
|
||||||
'mayan.apps.linking',
|
'mayan.apps.linking',
|
||||||
'mayan.apps.mailer',
|
'mayan.apps.mailer',
|
||||||
'mayan.apps.mayan_statistics',
|
'mayan.apps.mayan_statistics',
|
||||||
|
|||||||
Reference in New Issue
Block a user