Compare commits

..

2 Commits

Author SHA1 Message Date
Roberto Rosario
adf47646bf Initial commit for the document trashed event
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 08:29:45 -04:00
Roberto Rosario
4d7c0552bd Fix help text of the platformtemplate command
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-22 00:04:34 -04:00
69 changed files with 211 additions and 2038 deletions

View File

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

View File

@@ -1,14 +0,0 @@
- 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.

View File

@@ -1,19 +1,13 @@
Importer branch
===============
* Add a reusable task to upload documents.
* Add MVP of the importer app.
3.2.4 (2019-06-XX) 3.2.4 (2019-06-XX)
================== ==================
* 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.
3.2.3 (2019-06-21) 3.2.3 (2019-06-21)
================== ==================
* Add support for disabling the random primary key * Add support for disabling the random primary key
test mixin. test mixin.
* Add a reusable task to upload documents.
* Add MVP of the importer app.
* Fix mailing profile log columns mappings. * Fix mailing profile log columns mappings.
GitLab issue #626. Thanks to Jesaja Everling (@jeverling) GitLab issue #626. Thanks to Jesaja Everling (@jeverling)
for the report. for the report.

View File

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

View File

@@ -9,7 +9,7 @@ Changes
- 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.
Removals Removals
-------- --------

View File

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

View File

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

View File

@@ -21,13 +21,8 @@ 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
@@ -62,10 +57,6 @@ 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
@@ -121,10 +112,6 @@ 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 = {
@@ -150,10 +137,6 @@ class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
) )
template_name = 'authentication/password_reset_form.html' template_name = 'authentication/password_reset_form.html'
def post(self, *args, **kwargs):
event_user_password_reset_started.commit(actor=get_system_user())
return super(MayanPasswordResetView, self).post(*args, **kwargs)
class UserSetPasswordView(MultipleObjectFormActionView): class UserSetPasswordView(MultipleObjectFormActionView):
form_class = SetPasswordForm form_class = SetPasswordForm

View File

@@ -9,18 +9,16 @@ from mayan.apps.permissions.classes import Permission
from mayan.apps.smart_settings.classes import Namespace from mayan.apps.smart_settings.classes import Namespace
from .mixins import ( from .mixins import (
ClientMethodsTestCaseMixin, ConnectionsCheckTestCaseMixin, ClientMethodsTestCaseMixin, ContentTypeCheckTestCaseMixin,
ContentTypeCheckTestCaseMixin, ModelTestCaseMixin, ModelTestCaseMixin, OpenFileCheckTestCaseMixin,
OpenFileCheckTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin, RandomPrimaryKeyModelMonkeyPatchMixin, SilenceLoggerTestCaseMixin,
SilenceLoggerTestCaseMixin, TempfileCheckTestCasekMixin, TempfileCheckTestCasekMixin, TestViewTestCaseMixin
TestViewTestCaseMixin
) )
class BaseTestCase( class BaseTestCase(
SilenceLoggerTestCaseMixin, ConnectionsCheckTestCaseMixin, SilenceLoggerTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin,
RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin, ACLTestCaseMixin, ModelTestCaseMixin, OpenFileCheckTestCaseMixin,
ModelTestCaseMixin, OpenFileCheckTestCaseMixin,
TempfileCheckTestCasekMixin, TestCase TempfileCheckTestCasekMixin, TestCase
): ):
""" """

View File

@@ -12,7 +12,7 @@ from django.apps import apps
from django.conf import settings from django.conf import settings
from django.conf.urls import url from django.conf.urls import url
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import connection, connections, models from django.db import connection, models
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_save, pre_save
from django.http import HttpResponse from django.http import HttpResponse
from django.template import Context, Template from django.template import Context, Template
@@ -80,29 +80,6 @@ class ClientMethodsTestCaseMixin(object):
) )
class ConnectionsCheckTestCaseMixin(object):
_open_connections_check_enable = True
def _get_open_connections_count(self):
return len(connections.all())
def setUp(self):
super(ConnectionsCheckTestCaseMixin, self).setUp()
self._connections_count = self._get_open_connections_count()
def tearDown(self):
if self._open_connections_check_enable:
self.assertEqual(
self._connections_count, self._get_open_connections_count(),
msg='Database connection leak. The number of database '
'connections at the start and at the end of the test are not '
'the same.'
)
super(ConnectionsCheckTestCaseMixin, self).tearDown()
class ContentTypeCheckTestCaseMixin(object): class ContentTypeCheckTestCaseMixin(object):
expected_content_type = 'text/html; charset=utf-8' expected_content_type = 'text/html; charset=utf-8'

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-06-26 19:04
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('converter', '0013_auto_20180823_2353'),
]
operations = [
migrations.AlterField(
model_name='transformation',
name='name',
field=models.CharField(choices=[('crop', 'Crop: left, top, right, bottom'), ('draw_rectangle', 'Draw rectangle: left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('draw_rectangle_percent', 'Draw rectangle (percents coordinates): left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('flip', 'Flip'), ('gaussianblur', 'Gaussian blur: radius'), ('lineart', 'Line art'), ('mirror', 'Mirror'), ('resize', 'Resize: width, height'), ('rotate', 'Rotate: degrees, fillcolor'), ('rotate180', 'Rotate 180 degrees'), ('rotate270', 'Rotate 270 degrees'), ('rotate90', 'Rotate 90 degrees'), ('unsharpmask', 'Unsharp masking: radius, percent, threshold'), ('zoom', 'Zoom: percent')], max_length=128, verbose_name='Name'),
),
]

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import hashlib import hashlib
import logging import logging
from PIL import Image, ImageColor, ImageDraw, ImageFilter from PIL import Image, ImageColor, ImageFilter
from django.utils.translation import string_concat, ugettext_lazy as _ from django.utils.translation import string_concat, ugettext_lazy as _
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@@ -151,214 +151,6 @@ class TransformationCrop(BaseTransformation):
return self.image.crop((left, top, right, bottom)) return self.image.crop((left, top, right, bottom))
class TransformationDrawRectangle(BaseTransformation):
arguments = (
'left', 'top', 'right', 'bottom', 'fillcolor', 'outlinecolor',
'outlinewidth'
)
label = _('Draw rectangle')
name = 'draw_rectangle'
def execute_on(self, *args, **kwargs):
super(TransformationDrawRectangle, self).execute_on(*args, **kwargs)
try:
left = int(self.left or '0')
except ValueError:
left = 0
try:
top = int(self.top or '0')
except ValueError:
top = 0
try:
right = int(self.right or '0')
except ValueError:
right = 0
try:
bottom = int(self.bottom or '0')
except ValueError:
bottom = 0
if left < 0:
left = 0
if left > self.image.size[0] - 1:
left = self.image.size[0] - 1
if top < 0:
top = 0
if top > self.image.size[1] - 1:
top = self.image.size[1] - 1
if right < 0:
right = 0
if right > self.image.size[0] - 1:
right = self.image.size[0] - 1
if bottom < 0:
bottom = 0
if bottom > self.image.size[1] - 1:
bottom = self.image.size[1] - 1
# Invert right value
# Pillow uses left, top, right, bottom to define a viewport
# of real coordinates
# We invert the right and bottom to define a viewport
# that can crop from the right and bottom borders without
# having to know the real dimensions of an image
right = self.image.size[0] - right
bottom = self.image.size[1] - bottom
if left > right:
left = right - 1
if top > bottom:
top = bottom - 1
logger.debug(
'left: %f, top: %f, right: %f, bottom: %f', left, top, right,
bottom
)
fillcolor_value = getattr(self, 'fillcolor', None)
if fillcolor_value:
fill_color = ImageColor.getrgb(fillcolor_value)
else:
fill_color = 0
outlinecolor_value = getattr(self, 'outlinecolor', None)
if outlinecolor_value:
outline_color = ImageColor.getrgb(outlinecolor_value)
else:
outline_color = None
outlinewidth_value = getattr(self, 'outlinewidth', None)
if outlinewidth_value:
outline_width = int(outlinewidth_value)
else:
outline_width = 0
draw = ImageDraw.Draw(self.image)
draw.rectangle(
(left, top, right, bottom), fill=fill_color, outline=outline_color,
width=outline_width
)
return self.image
class TransformationDrawRectanglePercent(BaseTransformation):
arguments = (
'left', 'top', 'right', 'bottom', 'fillcolor', 'outlinecolor',
'outlinewidth'
)
label = _('Draw rectangle (percents coordinates)')
name = 'draw_rectangle_percent'
def execute_on(self, *args, **kwargs):
super(TransformationDrawRectanglePercent, self).execute_on(*args, **kwargs)
try:
left = float(self.left or '0')
except ValueError:
left = 0
try:
top = float(self.top or '0')
except ValueError:
top = 0
try:
right = float(self.right or '0')
except ValueError:
right = 0
try:
bottom = float(self.bottom or '0')
except ValueError:
bottom = 0
if left < 0:
left = 0
if left > 100:
left = 100
if top < 0:
top = 0
if top > 100:
top = 100
if right < 0:
right = 0
if right > 100:
right = 100
if bottom < 0:
bottom = 0
if bottom > 100:
bottom = 100
#if left > right:
# left, right = right, left
#if top > bottom:
# top, bottom = bottom, top
logger.debug(
'left: %f, top: %f, right: %f, bottom: %f', left, top, right,
bottom
)
fillcolor_value = getattr(self, 'fillcolor', None)
if fillcolor_value:
fill_color = ImageColor.getrgb(fillcolor_value)
else:
fill_color = 0
outlinecolor_value = getattr(self, 'outlinecolor', None)
if outlinecolor_value:
outline_color = ImageColor.getrgb(outlinecolor_value)
else:
outline_color = None
outlinewidth_value = getattr(self, 'outlinewidth', None)
if outlinewidth_value:
outline_width = int(outlinewidth_value)
else:
outline_width = 0
left = left / 100.0 * self.image.size[0]
top = top / 100.0 * self.image.size[1]
# Invert right value
# Pillow uses left, top, right, bottom to define a viewport
# of real coordinates
# We invert the right and bottom to define a viewport
# that can crop from the right and bottom borders without
# having to know the real dimensions of an image
right = self.image.size[0] - (right / 100.0 * self.image.size[0])
bottom = self.image.size[1] - (bottom / 100.0 * self.image.size[1])
draw = ImageDraw.Draw(self.image)
draw.rectangle(
(left, top, right, bottom), fill=fill_color, outline=outline_color,
width=outline_width
)
return self.image
class TransformationFlip(BaseTransformation): class TransformationFlip(BaseTransformation):
arguments = () arguments = ()
label = _('Flip') label = _('Flip')
@@ -524,8 +316,6 @@ class TransformationZoom(BaseTransformation):
BaseTransformation.register(transformation=TransformationCrop) BaseTransformation.register(transformation=TransformationCrop)
BaseTransformation.register(transformation=TransformationDrawRectangle)
BaseTransformation.register(transformation=TransformationDrawRectanglePercent)
BaseTransformation.register(transformation=TransformationFlip) BaseTransformation.register(transformation=TransformationFlip)
BaseTransformation.register(transformation=TransformationGaussianBlur) BaseTransformation.register(transformation=TransformationGaussianBlur)
BaseTransformation.register(transformation=TransformationLineArt) BaseTransformation.register(transformation=TransformationLineArt)

View File

@@ -31,10 +31,9 @@ from .html_widgets import (
) )
from .links import ( from .links import (
link_document_index_instance_list, link_document_type_index_templates, link_document_index_instance_list, link_document_type_index_templates,
link_index_instance_menu, link_index_instance_rebuild, link_index_instance_menu, link_index_template_setup,
link_index_template_setup, link_index_template_create, link_index_template_create, link_index_template_document_types,
link_index_template_document_types, link_index_template_delete, link_index_template_delete, link_index_template_edit, link_index_template_list,
link_index_template_edit, link_index_template_list,
link_index_template_node_tree_view, link_index_instances_rebuild, link_index_template_node_tree_view, link_index_instances_rebuild,
link_index_template_node_create, link_index_template_node_delete, link_index_template_node_create, link_index_template_node_delete,
link_index_template_node_edit link_index_template_node_edit
@@ -102,20 +101,15 @@ class DocumentIndexingApp(MayanAppConfig):
) )
SourceColumn( SourceColumn(
attribute='label', exclude=(IndexInstance,), is_identifier=True, attribute='label', is_identifier=True, is_sortable=True,
is_sortable=True, source=Index
)
SourceColumn(
attribute='label', is_object_absolute_url=True, is_identifier=True,
is_sortable=True, source=IndexInstance
)
SourceColumn(
attribute='slug', exclude=(IndexInstance,), is_sortable=True,
source=Index source=Index
) )
SourceColumn( SourceColumn(
attribute='enabled', exclude=(IndexInstance,), is_sortable=True, attribute='slug', is_sortable=True, source=Index
source=Index, widget=TwoStateWidget )
SourceColumn(
attribute='enabled', is_sortable=True, source=Index,
widget=TwoStateWidget
) )
SourceColumn( SourceColumn(
@@ -198,7 +192,6 @@ class DocumentIndexingApp(MayanAppConfig):
menu_object.bind_links( menu_object.bind_links(
links=( links=(
link_index_template_delete, link_index_template_edit, link_index_template_delete, link_index_template_edit,
link_index_instance_rebuild
), sources=(Index,) ), sources=(Index,)
) )
menu_object.bind_links( menu_object.bind_links(

View File

@@ -49,12 +49,6 @@ link_index_instances_rebuild = Link(
), ),
text=_('Rebuild indexes'), view='indexing:rebuild_index_instances' text=_('Rebuild indexes'), view='indexing:rebuild_index_instances'
) )
link_index_instance_rebuild = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_indexing.icons.icon_index_instances_rebuild',
permissions=(permission_document_indexing_rebuild,),
text=_('Rebuild index'), view='indexing:index_setup_rebuild'
)
link_index_template_setup = Link( link_index_template_setup = Link(
condition=get_cascade_condition( condition=get_cascade_condition(

View File

@@ -50,10 +50,3 @@ class IndexViewTestMixin(object):
'label': TEST_INDEX_LABEL_EDITED, 'slug': TEST_INDEX_SLUG 'label': TEST_INDEX_LABEL_EDITED, 'slug': TEST_INDEX_SLUG
} }
) )
def _request_test_index_rebuild_view(self):
return self.post(
viewname='indexing:index_setup_rebuild', kwargs={
'pk': self.test_index.pk
}
)

View File

@@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.documents.tests import GenericDocumentViewTestCase
from ..models import Index, IndexInstanceNode from ..models import Index
from ..permissions import ( from ..permissions import (
permission_document_indexing_create, permission_document_indexing_delete, permission_document_indexing_create, permission_document_indexing_delete,
permission_document_indexing_edit, permission_document_indexing_edit,
@@ -10,10 +10,7 @@ from ..permissions import (
permission_document_indexing_rebuild permission_document_indexing_rebuild
) )
from .literals import ( from .literals import TEST_INDEX_LABEL, TEST_INDEX_LABEL_EDITED
TEST_INDEX_LABEL, TEST_INDEX_LABEL_EDITED,
TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION
)
from .mixins import IndexTestMixin, IndexViewTestMixin from .mixins import IndexTestMixin, IndexViewTestMixin
@@ -103,27 +100,27 @@ class IndexViewTestCase(IndexTestMixin, IndexViewTestMixin, GenericDocumentViewT
) )
self.assertContains(response, text=TEST_INDEX_LABEL, status_code=200) self.assertContains(response, text=TEST_INDEX_LABEL, status_code=200)
def _request_indexes_rebuild_get_view(self): def _request_index_rebuild_get_view(self):
return self.get( return self.get(
viewname='indexing:rebuild_index_instances', viewname='indexing:rebuild_index_instances',
) )
def _request_indexes_rebuild_post_view(self): def _request_index_rebuild_post_view(self):
return self.post( return self.post(
viewname='indexing:rebuild_index_instances', data={ viewname='indexing:rebuild_index_instances', data={
'index_templates': self.test_index.pk 'index_templates': self.test_index.pk
} }
) )
def test_indexes_rebuild_no_permission(self): def test_index_rebuild_no_permission(self):
self._create_test_index(rebuild=False) self._create_test_index(rebuild=False)
response = self._request_indexes_rebuild_get_view() response = self._request_index_rebuild_get_view()
self.assertNotContains( self.assertNotContains(
response=response, text=self.test_index.label, status_code=200 response=response, text=self.test_index.label, status_code=200
) )
response = self._request_indexes_rebuild_post_view() response = self._request_index_rebuild_post_view()
# No error since we just don't see the index # No error since we just don't see the index
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -131,7 +128,7 @@ class IndexViewTestCase(IndexTestMixin, IndexViewTestMixin, GenericDocumentViewT
self.test_index.instance_root.get_children_count(), 0 self.test_index.instance_root.get_children_count(), 0
) )
def test_indexes_rebuild_with_access(self): def test_index_rebuild_with_access(self):
self._create_test_index(rebuild=False) self._create_test_index(rebuild=False)
self.grant_access( self.grant_access(
@@ -139,46 +136,13 @@ class IndexViewTestCase(IndexTestMixin, IndexViewTestMixin, GenericDocumentViewT
permission=permission_document_indexing_rebuild permission=permission_document_indexing_rebuild
) )
response = self._request_indexes_rebuild_get_view() response = self._request_index_rebuild_get_view()
self.assertContains( self.assertContains(
response=response, text=self.test_index.label, status_code=200 response=response, text=self.test_index.label, status_code=200
) )
response = self._request_indexes_rebuild_post_view() response = self._request_index_rebuild_post_view()
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
# An instance root exists # An instance root exists
self.assertTrue(self.test_index.instance_root.pk) self.assertTrue(self.test_index.instance_root.pk)
def test_index_rebuild_view_no_permission(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(IndexInstanceNode.objects.count(), 0)
def test_index_rebuild_view_with_access(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
self.grant_access(
obj=self.test_index,
permission=permission_document_indexing_rebuild
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(IndexInstanceNode.objects.count(), 0)

View File

@@ -11,8 +11,8 @@ from .views import (
DocumentIndexNodeListView, DocumentTypeIndexesView, IndexInstanceNodeView, DocumentIndexNodeListView, DocumentTypeIndexesView, IndexInstanceNodeView,
IndexListView, IndexesRebuildView, SetupIndexDocumentTypesView, IndexListView, IndexesRebuildView, SetupIndexDocumentTypesView,
SetupIndexCreateView, SetupIndexDeleteView, SetupIndexEditView, SetupIndexCreateView, SetupIndexDeleteView, SetupIndexEditView,
SetupIndexListView, SetupIndexRebuildView, SetupIndexTreeTemplateListView, SetupIndexListView, SetupIndexTreeTemplateListView, TemplateNodeCreateView,
TemplateNodeCreateView, TemplateNodeDeleteView, TemplateNodeEditView TemplateNodeDeleteView, TemplateNodeEditView
) )
urlpatterns = [ urlpatterns = [
@@ -46,10 +46,6 @@ urlpatterns = [
view=SetupIndexDocumentTypesView.as_view(), view=SetupIndexDocumentTypesView.as_view(),
name='index_setup_document_types' name='index_setup_document_types'
), ),
url(
regex=r'^setup/index/(?P<pk>\d+)/rebuild/$',
view=SetupIndexRebuildView.as_view(), name='index_setup_rebuild'
),
url( url(
regex=r'^setup/template/node/(?P<pk>\d+)/create/child/$', regex=r'^setup/template/node/(?P<pk>\d+)/create/child/$',
view=TemplateNodeCreateView.as_view(), name='template_node_create' view=TemplateNodeCreateView.as_view(), name='template_node_create'

View File

@@ -9,8 +9,8 @@ from django.utils.translation import ugettext_lazy as _, ungettext
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import ( from mayan.apps.common.generics import (
AddRemoveView, ConfirmView, FormView, SingleObjectCreateView, AddRemoveView, FormView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView SingleObjectEditView, SingleObjectListView
) )
from mayan.apps.documents.events import event_document_type_edited from mayan.apps.documents.events import event_document_type_edited
from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.models import Document, DocumentType
@@ -32,7 +32,7 @@ from .permissions import (
permission_document_indexing_create, permission_document_indexing_delete, permission_document_indexing_create, permission_document_indexing_delete,
permission_document_indexing_edit, permission_document_indexing_edit,
permission_document_indexing_instance_view, permission_document_indexing_instance_view,
permission_document_indexing_rebuild, permission_document_indexing_view permission_document_indexing_view
) )
from .tasks import task_rebuild_index from .tasks import task_rebuild_index
@@ -150,36 +150,6 @@ class SetupIndexListView(SingleObjectListView):
} }
class SetupIndexRebuildView(ConfirmView):
post_action_redirect = reverse_lazy(
viewname='indexing:index_setup_list'
)
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _('Rebuild index: %s') % self.get_object()
}
def get_object(self):
return get_object_or_404(klass=self.get_queryset(), pk=self.kwargs['pk'])
def get_queryset(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_indexing_rebuild,
queryset=Index.objects.all(), user=self.request.user
)
def view_action(self):
task_rebuild_index.apply_async(
kwargs=dict(index_id=self.get_object().pk)
)
messages.success(
message='Index queued for rebuild.', request=self.request
)
class SetupIndexDocumentTypesView(AddRemoveView): class SetupIndexDocumentTypesView(AddRemoveView):
main_object_method_add = 'document_types_add' main_object_method_add = 'document_types_add'
main_object_method_remove = 'document_types_remove' main_object_method_remove = 'document_types_remove'
@@ -309,7 +279,6 @@ class IndexListView(SingleObjectListView):
def get_extra_context(self): def get_extra_context(self):
return { return {
'hide_links': True, 'hide_links': True,
'hide_object': True,
'no_results_icon': icon_index, 'no_results_icon': icon_index,
'no_results_main_link': link_index_template_create.resolve( 'no_results_main_link': link_index_template_create.resolve(
context=RequestContext(request=self.request) context=RequestContext(request=self.request)

View File

@@ -161,8 +161,7 @@ class DocumentStatesApp(MayanAppConfig):
attribute='label', is_sortable=True, source=Workflow attribute='label', is_sortable=True, source=Workflow
) )
SourceColumn( SourceColumn(
attribute='internal_name', exclude=(WorkflowRuntimeProxy,), attribute='internal_name', is_sortable=True, source=Workflow
is_sortable=True, source=Workflow
) )
SourceColumn( SourceColumn(
attribute='get_initial_state', empty_value=_('None'), attribute='get_initial_state', empty_value=_('None'),
@@ -257,19 +256,6 @@ class DocumentStatesApp(MayanAppConfig):
) )
) )
SourceColumn(
source=WorkflowRuntimeProxy, label=_('Documents'),
func=lambda context: context['object'].get_document_count(
user=context['request'].user
), order=99
)
SourceColumn(
source=WorkflowStateRuntimeProxy, label=_('Documents'),
func=lambda context: context['object'].get_document_count(
user=context['request'].user
), order=99
)
menu_facet.bind_links( menu_facet.bind_links(
links=(link_document_workflow_instance_list,), sources=(Document,) links=(link_document_workflow_instance_list,), sources=(Document,)
) )

View File

@@ -17,7 +17,6 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.validators import validate_internal_name from mayan.apps.common.validators import validate_internal_name
from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.events.models import StoredEventType from mayan.apps.events.models import StoredEventType
from .error_logs import error_log_state_actions from .error_logs import error_log_state_actions
@@ -532,30 +531,9 @@ class WorkflowRuntimeProxy(Workflow):
verbose_name = _('Workflow runtime proxy') verbose_name = _('Workflow runtime proxy')
verbose_name_plural = _('Workflow runtime proxies') verbose_name_plural = _('Workflow runtime proxies')
def get_document_count(self, user):
"""
Return the numeric count of documents executing this workflow.
The count is filtered by access.
"""
return AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.filter(workflows__workflow=self),
user=user
).count()
class WorkflowStateRuntimeProxy(WorkflowState): class WorkflowStateRuntimeProxy(WorkflowState):
class Meta: class Meta:
proxy = True proxy = True
verbose_name = _('Workflow state runtime proxy') verbose_name = _('Workflow state runtime proxy')
verbose_name_plural = _('Workflow state runtime proxies') verbose_name_plural = _('Workflow state runtime proxies')
def get_document_count(self, user):
"""
Return the numeric count of documents at this workflow state.
The count is filtered by access.
"""
return AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=self.get_documents(),
user=user
).count()

View File

@@ -311,8 +311,15 @@ class DocumentsApp(MayanAppConfig):
source=DeletedDocument source=DeletedDocument
) )
SourceColumn( SourceColumn(
attribute='deleted_date_time', include_label=True, order=99, func=lambda context: document_page_thumbnail_widget.render(
source=DeletedDocument instance=context['object']
), label=_('Thumbnail'), source=DeletedDocument
)
SourceColumn(
attribute='document_type', is_sortable=True, source=DeletedDocument
)
SourceColumn(
attribute='deleted_date_time', source=DeletedDocument
) )
# DocumentVersion # DocumentVersion

View File

@@ -18,6 +18,9 @@ event_document_new_version = namespace.add_event_type(
event_document_properties_edit = namespace.add_event_type( event_document_properties_edit = namespace.add_event_type(
label=_('Document properties edited'), name='document_edit' label=_('Document properties edited'), name='document_edit'
) )
event_document_trashed = namespace.add_event_type(
label=_('Document moved to trash'), name='document_trashed'
)
# The type of an existing document is changed to another type # The type of an existing document is changed to another type
event_document_type_change = namespace.add_event_type( event_document_type_change = namespace.add_event_type(
label=_('Document type changed'), name='document_type_change' label=_('Document type changed'), name='document_type_change'

View File

@@ -41,7 +41,7 @@ class DocumentTypeFilteredSelectForm(forms.Form):
self.fields['document_type'] = field_class( self.fields['document_type'] = field_class(
help_text=help_text, label=_('Document type'), help_text=help_text, label=_('Document type'),
queryset=queryset, required=True, queryset=queryset, required=True,
widget=widget_class(attrs={'class': 'select2', 'size': 10}), **extra_kwargs widget=widget_class(attrs={'size': 10}), **extra_kwargs
) )

View File

@@ -32,7 +32,6 @@ DEFAULT_DOCUMENT_TYPE_LABEL = _('Default')
DOCUMENT_IMAGE_TASK_TIMEOUT = 120 DOCUMENT_IMAGE_TASK_TIMEOUT = 120
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'

View File

@@ -5,7 +5,7 @@ import uuid
from django.apps import apps from django.apps import apps
from django.core.files import File from django.core.files import File
from django.db import models from django.db import models, transaction
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now from django.utils.timezone import now
@@ -13,7 +13,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _
from ..events import ( from ..events import (
event_document_create, event_document_properties_edit, event_document_create, event_document_properties_edit,
event_document_type_change, event_document_trashed, event_document_type_change,
) )
from ..managers import DocumentManager, PassthroughManager, TrashCanManager from ..managers import DocumentManager, PassthroughManager, TrashCanManager
from ..settings import setting_language from ..settings import setting_language
@@ -104,11 +104,14 @@ class Document(models.Model):
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
to_trash = kwargs.pop('to_trash', True) to_trash = kwargs.pop('to_trash', True)
_user = kwargs.pop('_user', None)
if not self.in_trash and to_trash: if not self.in_trash and to_trash:
self.in_trash = True with transaction.atomic():
self.deleted_date_time = now() self.in_trash = True
self.save() self.deleted_date_time = now()
self.save()
event_document_trashed.commit(actor=_user, target=self)
else: else:
for version in self.versions.all(): for version in self.versions.all():
version.delete() version.delete()
@@ -177,21 +180,23 @@ class Document(models.Model):
user = kwargs.pop('_user', None) user = kwargs.pop('_user', None)
_commit_events = kwargs.pop('_commit_events', True) _commit_events = kwargs.pop('_commit_events', True)
new_document = not self.pk new_document = not self.pk
super(Document, self).save(*args, **kwargs)
if new_document: with transaction.atomic():
if user: super(Document, self).save(*args, **kwargs)
self.add_as_recent_document_for_user(user)
event_document_create.commit( if new_document:
actor=user, target=self, action_object=self.document_type if user:
) self.add_as_recent_document_for_user(user)
event_document_create.commit(
actor=user, target=self, action_object=self.document_type
)
else:
event_document_create.commit(
target=self, action_object=self.document_type
)
else: else:
event_document_create.commit( if _commit_events:
target=self, action_object=self.document_type event_document_properties_edit.commit(actor=user, target=self)
)
else:
if _commit_events:
event_document_properties_edit.commit(actor=user, target=self)
def save_to_file(self, *args, **kwargs): def save_to_file(self, *args, **kwargs):
return self.latest_version.save_to_file(*args, **kwargs) return self.latest_version.save_to_file(*args, **kwargs)
@@ -200,15 +205,16 @@ class Document(models.Model):
has_changed = self.document_type != document_type has_changed = self.document_type != document_type
self.document_type = document_type self.document_type = document_type
self.save() with transaction.atomic():
if has_changed or force: self.save()
post_document_type_change.send( if has_changed or force:
sender=self.__class__, instance=self post_document_type_change.send(
) sender=self.__class__, instance=self
)
event_document_type_change.commit(actor=_user, target=self) event_document_type_change.commit(actor=_user, target=self)
if _user: if _user:
self.add_as_recent_document_for_user(user=_user) self.add_as_recent_document_for_user(user=_user)
@property @property
def size(self): def size(self):

View File

@@ -82,7 +82,3 @@ queue_uploads.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_for', dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_for',
label=_('Scan document duplicates') label=_('Scan document duplicates')
) )
queue_uploads.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_upload_new_document',
label=_('Upload new document')
)

View File

@@ -9,8 +9,7 @@ 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_DOCUMENT_RETRY_DELAY, UPDATE_PAGE_COUNT_RETRY_DELAY, UPLOAD_NEW_VERSION_RETRY_DELAY
UPLOAD_NEW_VERSION_RETRY_DELAY
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -128,60 +127,6 @@ 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(

View File

@@ -228,9 +228,6 @@ class DocumentViewTestMixin(object):
data={'id_list': self.test_document.pk} data={'id_list': self.test_document.pk}
) )
def _request_empty_trash_view(self):
return self.post(viewname='documents:trash_can_empty')
def _request_document_print_view(self): def _request_document_print_view(self):
return self.get( return self.get(
viewname='documents:document_print', kwargs={ viewname='documents:document_print', kwargs={
@@ -239,3 +236,52 @@ class DocumentViewTestMixin(object):
'page_group': PAGE_RANGE_ALL 'page_group': PAGE_RANGE_ALL
} }
) )
class DocumentTrashViewMixin(object):
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')
def _request_document_restore_get_view(self):
return self.get(
viewname='documents:document_restore', kwargs={
'pk': self.test_document.pk
}
)
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_empty_trash_view(self):
return self.post(viewname='documents:trash_can_empty')
def _request_document_trash_post_view(self):
return self.post(
viewname='documents:document_trash', kwargs={
'pk': self.test_document.pk
}
)

View File

@@ -3,12 +3,16 @@ from __future__ import unicode_literals
from actstream.models import Action from actstream.models import Action
from django_downloadview import assert_download_response from django_downloadview import assert_download_response
from ..events import event_document_download, event_document_view from ..events import (
event_document_download, event_document_trashed, event_document_view
)
from ..permissions import ( from ..permissions import (
permission_document_download, permission_document_view permission_document_download, permission_document_trash,
permission_document_view
) )
from .base import GenericDocumentViewTestCase from .base import GenericDocumentViewTestCase
from .mixins import DocumentTrashViewMixin
TEST_DOCUMENT_TYPE_EDITED_LABEL = 'test document type edited label' TEST_DOCUMENT_TYPE_EDITED_LABEL = 'test document type edited label'
@@ -84,3 +88,20 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase):
self.assertEqual(event.actor, self._test_case_user) self.assertEqual(event.actor, self._test_case_user)
self.assertEqual(event.target, self.test_document) self.assertEqual(event.target, self.test_document)
self.assertEqual(event.verb, event_document_view.id) self.assertEqual(event.verb, event_document_view.id)
class DocumentTrashEventsTestCase(DocumentTrashViewMixin, GenericDocumentViewTestCase):
def test_document_trash_event_with_permissions(self):
Action.objects.all().delete()
self.grant_access(
obj=self.test_document, permission=permission_document_trash
)
response = self._request_document_trash_post_view()
self.assertEqual(response.status_code, 302)
event = Action.objects.any(obj=self.test_document).first()
#self.assertEqual(event.actor, self._test_case_user)
self.assertEqual(event.target, self.test_document)
self.assertEqual(event.verb, event_document_trashed.id)

View File

@@ -7,16 +7,10 @@ from ..permissions import (
) )
from .base import GenericDocumentViewTestCase from .base import GenericDocumentViewTestCase
from .mixins import DocumentTrashViewMixin
class TrashedDocumentTestCase(GenericDocumentViewTestCase): class TrashedDocumentTestCase(DocumentTrashViewMixin, GenericDocumentViewTestCase):
def _request_document_restore_get_view(self):
return self.get(
viewname='documents:document_restore', kwargs={
'pk': self.test_document.pk
}
)
def test_document_restore_get_view_no_permission(self): def test_document_restore_get_view_no_permission(self):
self.test_document.delete() self.test_document.delete()
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
@@ -43,13 +37,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(Document.objects.count(), document_count) self.assertEqual(Document.objects.count(), document_count)
def _request_document_restore_post_view(self):
return self.post(
viewname='documents:document_restore', kwargs={
'pk': self.test_document.pk
}
)
def test_document_restore_post_view_no_permission(self): def test_document_restore_post_view_no_permission(self):
self.test_document.delete() self.test_document.delete()
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
@@ -74,13 +61,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(DeletedDocument.objects.count(), 0) self.assertEqual(DeletedDocument.objects.count(), 0)
self.assertEqual(Document.objects.count(), 1) self.assertEqual(Document.objects.count(), 1)
def _request_document_trash_get_view(self):
return self.get(
viewname='documents:document_trash', kwargs={
'pk': self.test_document.pk
}
)
def test_document_trash_get_view_no_permissions(self): def test_document_trash_get_view_no_permissions(self):
document_count = Document.objects.count() document_count = Document.objects.count()
@@ -101,13 +81,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(Document.objects.count(), document_count) self.assertEqual(Document.objects.count(), document_count)
def _request_document_trash_post_view(self):
return self.post(
viewname='documents:document_trash', kwargs={
'pk': self.test_document.pk
}
)
def test_document_trash_post_view_no_permissions(self): def test_document_trash_post_view_no_permissions(self):
response = self._request_document_trash_post_view() response = self._request_document_trash_post_view()
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@@ -126,13 +99,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(DeletedDocument.objects.count(), 1) self.assertEqual(DeletedDocument.objects.count(), 1)
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
def _request_document_delete_get_view(self):
return self.get(
viewname='documents:document_delete', kwargs={
'pk': self.test_document.pk
}
)
def test_document_delete_get_view_no_permissions(self): def test_document_delete_get_view_no_permissions(self):
self.test_document.delete() self.test_document.delete()
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
@@ -165,13 +131,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
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)
@@ -198,9 +157,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase):
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()

View File

@@ -19,7 +19,6 @@ from .links import (
link_events_list, link_notification_mark_read, link_events_list, link_notification_mark_read,
link_notification_mark_read_all, link_user_notifications_list, link_notification_mark_read_all, link_user_notifications_list,
) )
from .utils import create_system_user
class EventsApp(MayanAppConfig): class EventsApp(MayanAppConfig):
@@ -102,5 +101,3 @@ 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
) )
create_system_user()

View File

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

View File

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

View File

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

View File

@@ -1,150 +0,0 @@
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']) as csv_datafile:
csv_reader = csv.reader(csv_datafile)
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']]) as file_object:
document_type_label = row[options['document_type_column']]
if document_type_label not in document_types:
self.stdout.write(
'New document type: {}. Creating and caching.'.format(
document_type_label
)
)
document_type, created = DocumentType.objects.get_or_create(
label=document_type_label
)
document_types[document_type_label] = document_type
else:
document_type = document_types[document_type_label]
shared_uploaded_file = SharedUploadedFile.objects.create(
file=File(file_object)
)
extra_data = {}
if options['metadata_pairs_column']:
extra_data['metadata_pairs'] = []
for pair in options['metadata_pairs_column'].split(','):
name, value = pair.split(':')
extra_data['metadata_pairs'].append(
{
'name': row[int(name)],
'value': row[int(value)]
}
)
task_upload_new_document.apply_async(
kwargs=dict(
document_type_id=document_type.pk,
shared_uploaded_file_id=shared_uploaded_file.pk,
extra_data=extra_data
)
)
uploaded_count = uploaded_count + 1
if (time.time() - time_last_display) > 1:
time_last_display = time.time()
self.stdout.write(
'Time: {}s, Files copied and queued: {}, files processed per second: {}'.format(
int(time.time() - time_start),
uploaded_count,
uploaded_count / (time.time() - time_start)
)
)
except (IOError, OSError) as exception:
if not options['ignore_errors']:
raise
else:
self.stderr.write(
'Error processing row: {}; {}.'.format(
row_count - 1, exception
)
)
self.stdout.write(
'Total files copied and queues: {}'.format(uploaded_count)
)
self.stdout.write(
'Total time: {}'.format(time.time() - time_start)
)

View File

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

View File

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

View File

@@ -1,120 +0,0 @@
from __future__ import unicode_literals
import csv
from django.core import management
from django.utils.encoding import force_bytes
from mayan.apps.documents.models import DocumentType, Document
from mayan.apps.documents.tests 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_file_descriptor, self.test_csv_path = mkstemp()
print('Test CSV file: {}'.format(self.test_csv_path))
with open(self.test_csv_path, mode='wb') as csvfile:
filewriter = csv.writer(
csvfile, delimiter=force_bytes(','), quotechar=force_bytes('"'),
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,
file_descriptor=self.test_csv_file_descriptor
)
def test_import_csv_read(self):
self.test_document_type.delete()
management.call_command('import', self.test_csv_path)
self.assertTrue(DocumentType.objects.count() > 0)
self.assertTrue(Document.objects.count() > 0)
def test_import_document_type_column_mapping(self):
self.test_document_type.delete()
management.call_command(
'import', self.test_csv_path, '--document_type_column', '2'
)
self.assertTrue(DocumentType.objects.first().label == 'column 2')
self.assertTrue(Document.objects.count() > 0)
def test_import_document_path_column_mapping(self):
self.test_document_type.delete()
with self.assertRaises(IOError):
management.call_command(
'import', self.test_csv_path, '--document_path_column', '2'
)
def test_import_metadata_column_mapping(self):
self.test_document_type.delete()
management.call_command(
'import', self.test_csv_path, '--metadata_pairs_column', '2:3,4:5',
)
self.assertTrue(DocumentType.objects.count() > 0)
self.assertTrue(Document.objects.count() > 0)
self.assertTrue(Document.objects.first().metadata.count() > 0)
self.assertEqual(
Document.objects.first().metadata.get(
metadata_type__name='column_2'
).value, 'column 3'
)
def test_import_ambiguous_metadata(self):
self.auto_generate_test_csv_file = False
self.test_import_count = 2
self.test_document_type.delete()
management.call_command(
'import', self.test_csv_path, '--metadata_pairs_column', '6:7',
)
self.assertTrue(DocumentType.objects.count() > 0)
self.assertTrue(Document.objects.count() > 0)
self.assertTrue(Document.objects.first().metadata.count() > 0)
self.assertEqual(
Document.objects.first().metadata.get(
metadata_type__name='part'
).value, 'value'
)

View File

@@ -1,10 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
TEST_BODY_HTML = '<strong>test body</strong>'
TEST_EMAIL_ADDRESS = 'test@example.com' TEST_EMAIL_ADDRESS = 'test@example.com'
TEST_EMAIL_BODY = 'test body'
TEST_EMAIL_BODY_HTML = '<strong>test body</strong>'
TEST_EMAIL_FROM_ADDRESS = 'from.test@example.com' TEST_EMAIL_FROM_ADDRESS = 'from.test@example.com'
TEST_EMAIL_SUBJECT = 'test subject'
TEST_RECIPIENTS_MULTIPLE_COMMA = 'test@example.com,test2@example.com' TEST_RECIPIENTS_MULTIPLE_COMMA = 'test@example.com,test2@example.com'
TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT = [ TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT = [
'test@example.com', 'test2@example.com' 'test@example.com', 'test2@example.com'

View File

@@ -1,180 +0,0 @@
from __future__ import unicode_literals
import json
from django.core import mail
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.documents.tests.mixins import DocumentTestMixin
from mayan.apps.document_states.literals import WORKFLOW_ACTION_ON_ENTRY
from mayan.apps.document_states.tests.mixins import WorkflowTestMixin
from mayan.apps.document_states.tests.test_actions import ActionTestCase
from mayan.apps.metadata.tests.mixins import MetadataTypeTestMixin
from ..permissions import permission_user_mailer_use
from ..workflow_actions import EmailAction
from .literals import (
TEST_EMAIL_ADDRESS, TEST_EMAIL_BODY, TEST_EMAIL_FROM_ADDRESS,
TEST_EMAIL_SUBJECT
)
from .mixins import MailerTestMixin
class EmailActionTestCase(MailerTestMixin, WorkflowTestMixin, ActionTestCase):
def test_email_action_literal_text(self):
self._create_test_user_mailer()
action = EmailAction(
form_data={
'mailing_profile': self.test_user_mailer.pk,
'recipient': TEST_EMAIL_ADDRESS,
'subject': TEST_EMAIL_SUBJECT,
'body': TEST_EMAIL_BODY,
}
)
action.execute(context={'document': self.test_document})
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS)
self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS])
def test_email_action_workflow_execute(self):
self._create_test_workflow()
self._create_test_workflow_state()
self._create_test_user_mailer()
self.test_workflow_state.actions.create(
action_data=json.dumps(
{
'mailing_profile': self.test_user_mailer.pk,
'recipient': TEST_EMAIL_ADDRESS,
'subject': TEST_EMAIL_SUBJECT,
'body': TEST_EMAIL_BODY,
}
),
action_path='mayan.apps.mailer.workflow_actions.EmailAction',
label='test email action', when=WORKFLOW_ACTION_ON_ENTRY,
)
self.test_workflow_state.initial = True
self.test_workflow_state.save()
self.test_workflow.document_types.add(self.test_document_type)
self.upload_document()
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS)
self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS])
class EmailActionTemplateTestCase(MetadataTypeTestMixin, MailerTestMixin, WorkflowTestMixin, ActionTestCase):
def test_email_action_recipient_template(self):
self._create_test_metadata_type()
self.test_document_type.metadata.create(metadata_type=self.test_metadata_type)
self.test_document.metadata.create(metadata_type=self.test_metadata_type, value=TEST_EMAIL_ADDRESS)
self._create_test_user_mailer()
action = EmailAction(
form_data={
'mailing_profile': self.test_user_mailer.pk,
'recipient': '{{{{ document.metadata_value_of.{} }}}}'.format(self.test_metadata_type.name),
'subject': TEST_EMAIL_SUBJECT,
'body': '',
}
)
action.execute(context={'document': self.test_document})
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS)
self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS])
def test_email_action_subject_template(self):
self._create_test_metadata_type()
self.test_document_type.metadata.create(metadata_type=self.test_metadata_type)
self.test_document.metadata.create(metadata_type=self.test_metadata_type, value=TEST_EMAIL_SUBJECT)
self._create_test_user_mailer()
action = EmailAction(
form_data={
'mailing_profile': self.test_user_mailer.pk,
'recipient': TEST_EMAIL_ADDRESS,
'subject': '{{{{ document.metadata_value_of.{} }}}}'.format(self.test_metadata_type.name),
'body': '',
}
)
action.execute(context={'document': self.test_document})
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS)
self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS])
def test_email_action_body_template(self):
self._create_test_metadata_type()
self.test_document_type.metadata.create(metadata_type=self.test_metadata_type)
self.test_document.metadata.create(metadata_type=self.test_metadata_type, value=TEST_EMAIL_BODY)
self._create_test_user_mailer()
action = EmailAction(
form_data={
'mailing_profile': self.test_user_mailer.pk,
'recipient': TEST_EMAIL_ADDRESS,
'subject': TEST_EMAIL_SUBJECT,
'body': '{{{{ document.metadata_value_of.{} }}}}'.format(self.test_metadata_type.name),
}
)
action.execute(context={'document': self.test_document})
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS)
self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS])
self.assertEqual(mail.outbox[0].body, TEST_EMAIL_BODY)
class EmailActionViewTestCase(DocumentTestMixin, MailerTestMixin, WorkflowTestMixin, GenericViewTestCase):
auto_upload_document = False
def test_email_action_create_get_view(self):
self._create_test_workflow()
self._create_test_workflow_state()
self._create_test_user_mailer()
response = self.get(
viewname='document_states:setup_workflow_state_action_create',
kwargs={
'pk': self.test_workflow_state.pk,
'class_path': 'mayan.apps.mailer.workflow_actions.EmailAction',
}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(self.test_workflow_state.actions.count(), 0)
def _request_email_action_create_post_view(self):
return self.post(
viewname='document_states:setup_workflow_state_action_create',
kwargs={
'pk': self.test_workflow_state.pk,
'class_path': 'mayan.apps.mailer.workflow_actions.EmailAction',
}, data={
'when': WORKFLOW_ACTION_ON_ENTRY,
'label': 'test email action',
'mailing_profile': self.test_user_mailer.pk,
'recipient': TEST_EMAIL_ADDRESS,
'subject': TEST_EMAIL_SUBJECT,
'body': TEST_EMAIL_BODY,
}
)
def test_email_action_create_post_view(self):
self._create_test_workflow()
self._create_test_workflow_state()
self._create_test_user_mailer()
self.grant_access(
obj=self.test_user_mailer, permission=permission_user_mailer_use
)
response = self._request_email_action_create_post_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(self.test_workflow_state.actions.count(), 1)

View File

@@ -5,7 +5,7 @@ from django.core import mail
from mayan.apps.documents.tests.test_models import GenericDocumentTestCase from mayan.apps.documents.tests.test_models import GenericDocumentTestCase
from .literals import ( from .literals import (
TEST_EMAIL_BODY_HTML, TEST_EMAIL_ADDRESS, TEST_EMAIL_FROM_ADDRESS, TEST_BODY_HTML, TEST_EMAIL_ADDRESS, TEST_EMAIL_FROM_ADDRESS,
TEST_RECIPIENTS_MULTIPLE_COMMA, TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT, TEST_RECIPIENTS_MULTIPLE_COMMA, TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT,
TEST_RECIPIENTS_MULTIPLE_SEMICOLON, TEST_RECIPIENTS_MULTIPLE_SEMICOLON,
TEST_RECIPIENTS_MULTIPLE_SEMICOLON_RESULT, TEST_RECIPIENTS_MULTIPLE_MIXED, TEST_RECIPIENTS_MULTIPLE_SEMICOLON_RESULT, TEST_RECIPIENTS_MULTIPLE_MIXED,
@@ -25,22 +25,17 @@ class ModelTestCase(MailerTestMixin, GenericDocumentTestCase):
def test_send_simple_with_html(self): def test_send_simple_with_html(self):
self._create_test_user_mailer() self._create_test_user_mailer()
self.test_user_mailer.send( self.test_user_mailer.send(to=TEST_EMAIL_ADDRESS, body=TEST_BODY_HTML)
to=TEST_EMAIL_ADDRESS, body=TEST_EMAIL_BODY_HTML
)
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS)
self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS])
self.assertEqual( self.assertEqual(mail.outbox[0].alternatives[0][0], TEST_BODY_HTML)
mail.outbox[0].alternatives[0][0], TEST_EMAIL_BODY_HTML
)
def test_send_attachment(self): def test_send_attachment(self):
self._create_test_user_mailer() self._create_test_user_mailer()
self.test_user_mailer.send_document( self.test_user_mailer.send_document(
to=TEST_EMAIL_ADDRESS, document=self.test_document, to=TEST_EMAIL_ADDRESS, document=self.test_document, as_attachment=True
as_attachment=True
) )
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)

View File

@@ -1,124 +0,0 @@
from __future__ import absolute_import, unicode_literals
import logging
from django.template import Template, Context
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.document_states.classes import WorkflowAction
from mayan.apps.document_states.exceptions import WorkflowStateActionError
from .models import UserMailer
from .permissions import permission_user_mailer_use
__all__ = ('EmailAction',)
logger = logging.getLogger(__name__)
class EmailAction(WorkflowAction):
fields = {
'mailing_profile': {
'label': _('Mailing profile'),
'class': 'django.forms.ModelChoiceField', 'kwargs': {
'help_text': _('Mailing profile to use when sending the email.'),
'queryset': UserMailer.objects.none(), 'required': True
}
},
'recipient': {
'label': _('Recipient'),
'class': 'django.forms.CharField', 'kwargs': {
'help_text': _(
'Email address of the recipient. Can be multiple addresses '
'separated by comma or semicolon. A template can be used '
'to reference properties of the document.'
),
'required': True
}
},
'subject': {
'label': _('Subject'),
'class': 'django.forms.CharField', 'kwargs': {
'help_text': _(
'Subject of the email. Can be a string or a template.'
),
'required': True
}
},
'body': {
'label': _('Body'),
'class': 'django.forms.CharField', 'kwargs': {
'help_text': _(
'Body of the email to send. Can be a string or a template.'
),
'required': True
}
},
}
field_order = ('mailing_profile', 'recipient', 'subject', 'body')
label = _('Send email')
widgets = {
'body': {
'class': 'django.forms.widgets.Textarea', 'kwargs': {}
}
}
permission = permission_user_mailer_use
def execute(self, context):
try:
recipient = Template(self.form_data['recipient']).render(
context=Context(context)
)
except Exception as exception:
raise WorkflowStateActionError(
_('Recipient template error: %s') % exception
)
else:
logger.debug('Recipient result: %s', recipient)
try:
subject = Template(self.form_data['subject']).render(
context=Context(context)
)
except Exception as exception:
raise WorkflowStateActionError(
_('Subject template error: %s') % exception
)
else:
logger.debug('Subject result: %s', subject)
try:
body = Template(self.form_data['body']).render(
context=Context(context)
)
except Exception as exception:
raise WorkflowStateActionError(
_('Body template error: %s') % exception
)
else:
logger.debug('Body result: %s', body)
user_mailer = self.get_user_mailer()
user_mailer.send(
to=recipient, subject=subject, body=body,
)
def get_form_schema(self, request):
user = request.user
logger.debug('user: %s', user)
queryset = AccessControlList.objects.restrict_queryset(
permission=self.permission, queryset=UserMailer.objects.all(),
user=user
)
self.fields['mailing_profile']['kwargs']['queryset'] = queryset
return {
'field_order': self.field_order,
'fields': self.fields,
'widgets': self.widgets
}
def get_user_mailer(self):
return UserMailer.objects.get(pk=self.form_data['mailing_profile'])

View File

@@ -577,65 +577,44 @@ class SourceColumn(object):
@classmethod @classmethod
def get_for_source(cls, context, source, exclude_identifier=False, only_identifier=False): def get_for_source(cls, context, source, exclude_identifier=False, only_identifier=False):
columns = []
source_classes = set()
if hasattr(source, '_meta'):
source_classes.add(source._meta.model)
else:
source_classes.add(source)
try: try:
columns.extend(cls._registry[source]) result = cls._registry[source]
except KeyError:
pass
try:
# Might be an instance, try its class
columns.extend(cls._registry[source.__class__])
except KeyError: except KeyError:
try: try:
# Might be a subclass, try its root class # Might be an instance, try its class
columns.extend(cls._registry[source.__class__.__mro__[-2]]) result = cls._registry[source.__class__]
except KeyError: except KeyError:
pass try:
# Might be a subclass, try its root class
result = cls._registry[source.__class__.__mro__[-2]]
except KeyError:
try:
# Might be an inherited class insance, try its source class
result = cls._registry[source.source_ptr.__class__]
except (KeyError, AttributeError):
try:
# Try it as a queryset
result = cls._registry[source.model]
except AttributeError:
try:
# Special case for queryset items produced from
# .defer() or .only() optimizations
result = cls._registry[list(source._meta.parents.items())[0][0]]
except (AttributeError, KeyError, IndexError):
result = ()
except TypeError:
# unhashable type: list
result = ()
try: result = SourceColumn.sort(columns=result)
# Might be an inherited class instance, try its source class
columns.extend(cls._registry[source.source_ptr.__class__])
except (KeyError, AttributeError):
pass
try:
# Try it as a queryset
columns.extend(cls._registry[source.model])
except AttributeError:
pass
try:
# Special case for queryset items produced from
# .defer() or .only() optimizations
result = cls._registry[list(source._meta.parents.items())[0][0]]
except (AttributeError, KeyError, IndexError):
pass
else:
# Second level special case for model subclasses from
# .defer and .only querysets
# Examples: Workflow runtime proxy and index instances in 3.2.x
for column in result:
if not source_classes.intersection(set(column.exclude)):
columns.append(column)
columns = SourceColumn.sort(columns=columns)
if exclude_identifier: if exclude_identifier:
columns = [column for column in columns if not column.is_identifier] result = [item for item in result if not item.is_identifier]
else: else:
if only_identifier: if only_identifier:
for column in columns: for item in result:
if column.is_identifier: if item.is_identifier:
return column return item
return None return None
final_result = [] final_result = []
@@ -653,20 +632,20 @@ class SourceColumn(object):
logger.warning( logger.warning(
'No request variable, aborting request resolution' 'No request variable, aborting request resolution'
) )
return final_result return result
current_view_name = get_current_view_name(request=request) current_view_name = get_current_view_name(request=request)
for column in columns: for item in result:
if column.views: if item.views:
if current_view_name in column.views: if current_view_name in item.views:
final_result.append(column) final_result.append(item)
else: else:
final_result.append(column) final_result.append(item)
return final_result return final_result
def __init__( def __init__(
self, source, attribute=None, empty_value=None, exclude=None, func=None, self, source, attribute=None, empty_value=None, func=None,
include_label=False, is_attribute_absolute_url=False, include_label=False, is_attribute_absolute_url=False,
is_object_absolute_url=False, is_identifier=False, is_sortable=False, is_object_absolute_url=False, is_identifier=False, is_sortable=False,
kwargs=None, label=None, order=None, sort_field=None, views=None, kwargs=None, label=None, order=None, sort_field=None, views=None,
@@ -676,7 +655,6 @@ class SourceColumn(object):
self._label = label self._label = label
self.attribute = attribute self.attribute = attribute
self.empty_value = empty_value self.empty_value = empty_value
self.exclude = exclude or ()
self.func = func self.func = func
self.is_attribute_absolute_url = is_attribute_absolute_url self.is_attribute_absolute_url = is_attribute_absolute_url
self.is_object_absolute_url = is_object_absolute_url self.is_object_absolute_url = is_object_absolute_url

View File

@@ -113,6 +113,12 @@ def navigation_source_column_get_absolute_url(source_column, obj):
return source_column.get_absolute_url(obj=obj) return source_column.get_absolute_url(obj=obj)
@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:
@@ -120,9 +126,3 @@ def navigation_source_column_resolve(context, column):
return result return result
else: else:
return '' return ''
@register.simple_tag(takes_context=True)
def resolve_link(context, link):
# This can be used to resolve links or menus too
return link.resolve(context=context)

View File

@@ -16,7 +16,8 @@ class Command(management.BaseCommand):
) )
parser.add_argument( parser.add_argument(
'--context', action='store', default='', dest='context', '--context', action='store', default='', dest='context',
help='Show a list of available templates.', help='Pass a context to the template in the form of a JSON encoded '
'dictionary.',
) )
def handle(self, *args, **options): def handle(self, *args, **options):

View File

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

View File

@@ -1,56 +0,0 @@
from __future__ import unicode_literals
import logging
from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.menus import (
menu_list_facet, menu_object, menu_secondary,
)
from .dependencies import * # NOQA
from .links import (
link_redaction_create, link_redaction_delete, link_redaction_edit,
link_redaction_list
)
logger = logging.getLogger(__name__)
class RedactionsApp(MayanAppConfig):
app_namespace = 'redactions'
app_url = 'redactions'
has_rest_api = False
has_tests = False
name = 'mayan.apps.redactions'
verbose_name = _('Redactions')
def ready(self):
super(RedactionsApp, self).ready()
DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage'
)
Redaction = self.get_model(model_name='Redaction')
menu_list_facet.bind_links(
links=(
link_redaction_list,
), sources=(DocumentPage,)
)
menu_object.bind_links(
links=(link_redaction_delete, link_redaction_edit,),
sources=(Redaction,)
)
menu_secondary.bind_links(
links=(link_redaction_create,), sources=(Redaction,)
)
menu_secondary.bind_links(
links=(link_redaction_create,),
sources=(
'redactions:redaction_create',
'redactions:redaction_list'
)
)

View File

@@ -1,28 +0,0 @@
'''
from __future__ import unicode_literals
import logging
from PIL import Image
from converter import converter_class
logger = logging.getLogger(__name__)
class OCRBackendBase(object):
def execute(self, file_object, language=None, process_barcodes=True, process_text=True, transformations=None):
self.language = language
self.process_barcodes = process_barcodes
self.process_text = process_text
if not transformations:
transformations = []
self.converter = converter_class(file_object=file_object)
for transformation in transformations:
self.converter.transform(transformation=transformation)
self.image = Image.open(self.converter.get_page())
'''

View File

@@ -1,15 +0,0 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.dependencies.classes import (
GoogleFontDependency, JavaScriptDependency
)
JavaScriptDependency(
label=_('JavaScript image cropper'), module=__name__, name='cropperjs',
version_string='=1.4.1'
)
JavaScriptDependency(
module=__name__, name='jquery-cropper', version_string='=1.0.0'
)

View File

@@ -1,16 +0,0 @@
'''from __future__ import unicode_literals
class OCRError(Exception):
"""
Raised by the OCR backend
"""
pass
class UnpaperError(Exception):
"""
Raised by unpaper
"""
pass
'''

View File

@@ -1,15 +0,0 @@
from __future__ import unicode_literals
from django import forms
from django.utils.translation import ugettext_lazy as _
from .models import Redaction
class RedactionCoordinatesForm(forms.ModelForm):
class Meta:
fields = ('arguments',)
model = Redaction
widgets = {
'arguments': forms.widgets.Textarea(attrs={'class': 'hidden'}),
}

View File

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

View File

@@ -1,32 +0,0 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.navigation.classes import Link
from .permissions import (
permission_redaction_create, permission_redaction_delete,
permission_redaction_edit, permission_redaction_view
)
link_redaction_create = Link(
icon_class_path='mayan.apps.redactions.icons.icon_redaction_create',
permissions=(permission_redaction_create,), text=_('Create redaction'),
view='redactions:redaction_create', args='resolved_object.id'
)
link_redaction_delete = Link(
icon_class_path='mayan.apps.redactions.icons.icon_redaction_delete',
permissions=(permission_redaction_delete,), tags='dangerous',
text=_('Delete'), view='redactions:redaction_delete',
args='resolved_object.id'
)
link_redaction_edit = Link(
icon_class_path='mayan.apps.redactions.icons.icon_redaction_edit',
permissions=(permission_redaction_edit,), text=_('Edit'),
view='redactions:redaction_edit', args='resolved_object.id'
)
link_redaction_list = Link(
icon_class_path='mayan.apps.redactions.icons.icon_redactions',
permissions=(permission_redaction_view,), text=_('Redactions'),
view='redactions:redaction_list', args='resolved_object.id'
)

View File

@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-06-26 19:04
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
initial = True
dependencies = [
('converter', '0014_auto_20190626_1904'),
]
operations = [
migrations.CreateModel(
name='Redaction',
fields=[
],
options={
'verbose_name': 'Redaction',
'proxy': True,
'verbose_name_plural': 'Redactions',
'indexes': [],
},
bases=('converter.transformation',),
),
]

View File

@@ -1,12 +0,0 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.converter.models import Transformation
class Redaction(Transformation):
class Meta:
proxy = True
verbose_name = _('Redaction')
verbose_name_plural = _('Redactions')

View File

@@ -1,20 +0,0 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Redactions'), name='redactions')
permission_redaction_create = namespace.add_permission(
label=_('Create new redactions'), name='redaction_create'
)
permission_redaction_delete = namespace.add_permission(
label=_('Delete redactions'), name='redaction_delete'
)
permission_redaction_edit = namespace.add_permission(
label=_('Edit redactions'), name='redaction_edit'
)
permission_redaction_view = namespace.add_permission(
label=_('View existing redactions'), name='redaction_view'
)

View File

@@ -1,5 +0,0 @@
from rest_framework import serializers
class DocumentVersionOCRSerializer(serializers.Serializer):
document_version_id = serializers.IntegerField()

View File

@@ -1,113 +0,0 @@
{% extends 'appearance/base.html' %}
{% load i18n %}
{% load static %}
{% load common_tags %}
{% block title %}{% blocktrans with object as object %}Redaction coordinates for: {{ object }}{% endblocktrans %}{% endblock title %}
{% block stylesheets %}
<link href="{% static 'redactions/node_modules/cropperjs/dist/cropper.css' %}" rel="stylesheet">
<style>
.cropper-main {
width: 100%;
}
.cropper-main img {
max-width: 100%;
}
</style>
{% endblock %}
{% block content %}
<div class="cropper-main">
<img src="{{ document_page.get_api_image_url }}">
</div>
<br>
{% with '' as title %}
{% include 'appearance/generic_form_subtemplate.html' %}
{% endwith %}
{% endblock content %}
{% block javascript %}
<script>
var crop_left, crop_top, crop_right, crop_bottom;
var pic_real_width, pic_real_height;
var canvasData;
var containerData;
var $image = $('.cropper-main img');
var cropperInstance;
var defaultArguments = {
left: 10,
top: 10,
right: 10,
bottom: 10,
fillcolor: '#000000',
}
var initialArguments = JSON.parse($('#id_arguments').text() || JSON.stringify(defaultArguments));
var callbackCrop = function (data) {
var crop_left = (data.detail.x / pic_real_width * 100).toFixed(2);
var crop_top = (data.detail.y / pic_real_height * 100).toFixed(2);
var crop_right = (100.001 - (data.detail.x + data.detail.width) / pic_real_width * 100).toFixed(2);
var crop_bottom = (100.001 - (data.detail.y + data.detail.height) / pic_real_height * 100).toFixed(2);
var arguments = {
'left': parseFloat(crop_left),
'top': parseFloat(crop_top),
'right': parseFloat(crop_right),
'bottom': parseFloat(crop_bottom),
'fillcolor': '#000000',
}
$('#id_arguments').text(JSON.stringify(arguments));
}
jQuery(document).ready(function() {
$('.help-block').hide();
$('label').hide();
});
$.getScript("{% static 'redactions/node_modules/cropperjs/dist/cropper.js' %}")
.done(function (script, textStatus) {
$.getScript("{% static 'redactions/node_modules/jquery-cropper/dist/jquery-cropper.js' %}")
.done(function (script, textStatus) {
jQuery(document).ready(function () {
// Create DOM new image to get the real
// (unscaled) image size
$('<img/>')
.attr('src', $image.attr('src'))
.on('load', function () {
pic_real_width = this.width;
pic_real_height = this.height;
});
cropperInstance = $image.cropper({
crop: callbackCrop,
mouseWheelZoom: false,
movable: false,
//preview: '.cropper-preview',
ready: function () {
canvasData = $image.cropper('getCanvasData');
containerData = $image.cropper('getContainerData');
$image.cropper('setCropBoxData', {
left: initialArguments.left / 100.0 * canvasData.width + canvasData.left,
top: initialArguments.top / 100.0 * canvasData.height + canvasData.top,
width: (100.0 - initialArguments.right - initialArguments.left) / 100.0 * canvasData.width,
height: (100.0 - initialArguments.bottom - initialArguments.top) / 100.0 * canvasData.height,
});
},
rotatable: false,
touchDragZoom: false,
viewMode: 1,
zoomable: false,
});
})
})
});
</script>
{% endblock %}

View File

@@ -1,30 +0,0 @@
from __future__ import unicode_literals
from django.conf.urls import url
from .views import (
RedactionCreateView, RedactionDeleteView, RedactionEditView,
RedactionListView,
)
urlpatterns = [
url(
regex=r'^document_pages/(?P<pk>\d+)/redactions/create/$',
view=RedactionCreateView.as_view(), name='redaction_create'
),
url(
regex=r'^document_pages/(?P<pk>\d+)/redactions/$',
view=RedactionListView.as_view(), name='redaction_list'
),
url(
regex=r'^redactions/(?P<pk>\d+)/delete/$',
view=RedactionDeleteView.as_view(), name='redaction_delete'
),
url(
regex=r'^redactions/(?P<pk>\d+)/edit/$',
view=RedactionEditView.as_view(), name='redaction_edit'
),
]
api_urls = []

View File

@@ -1,147 +0,0 @@
from __future__ import absolute_import, unicode_literals
import logging
from django.core.urlresolvers import reverse
from django.template import RequestContext
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import (
SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView,
SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.converter.transformations import TransformationDrawRectanglePercent
from mayan.apps.documents.models import DocumentPage
from .forms import RedactionCoordinatesForm
from .icons import icon_redactions
from .links import link_redaction_create
from .models import Redaction
from .permissions import (
permission_redaction_create, permission_redaction_delete,
permission_redaction_edit, permission_redaction_view
)
logger = logging.getLogger(__name__)
class RedactionCreateView(ExternalObjectMixin, SingleObjectCreateView):
external_object_class = DocumentPage
external_object_pk_url_kwarg = 'pk'
form_class = RedactionCoordinatesForm
model = Redaction
object_permission = permission_redaction_create
template_name = 'redactions/cropper.html'
def form_valid(self, form):
instance = form.save(commit=False)
instance.content_object = self.external_object
instance.name = TransformationDrawRectanglePercent.name
instance.save()
return super(RedactionCreateView, self).form_valid(form)
def get_extra_context(self, **kwargs):
context = {
'document_page': self.external_object,
'redaction': self.object,
'title': _('Create redaction for: %s') % self.external_object
}
return context
def get_post_action_redirect(self):
return reverse(
viewname='redactions:redaction_list', kwargs={
'pk': self.external_object.pk
}
)
class RedactionDeleteView(SingleObjectDeleteView):
model = Redaction
object_permission = permission_redaction_delete
def get_post_action_redirect(self):
return reverse(
viewname='redactions:redaction_list', kwargs={
'pk': self.object.content_object.pk
}
)
def get_extra_context(self):
return {
'content_object': self.object.content_object,
'navigation_object_list': ('content_object', 'redaction'),
'previous': reverse(
viewname='redactions:redaction_list', kwargs={
'pk': self.object.content_object.pk
}
),
'redaction': self.object,
'title': _(
'Delete refaction for: %(content_object)s?'
) % {
'content_object': self.object.content_object
},
}
class RedactionEditView(SingleObjectEditView):
form_class = RedactionCoordinatesForm
model = Redaction
object_permission = permission_redaction_edit
template_name = 'redactions/cropper.html'
def get_extra_context(self, **kwargs):
context = {
'document_page': self.object.content_object,
'navigation_object_list': ['document_page', 'redaction'],
'redaction': self.object,
'title': _('Edit redaction: %s') % self.object
}
return context
def get_post_action_redirect(self):
return reverse(
viewname='redactions:redaction_list', kwargs={
'pk': self.object.content_object.pk
}
)
class RedactionListView(ExternalObjectMixin, SingleObjectListView):
external_object_class = DocumentPage
object_permission = permission_redaction_view
external_object_pk_url_kwarg = 'pk'
def dispatch(self, request, *args, **kwargs):
return super(RedactionListView, self).dispatch(
request, *args, **kwargs
)
def get_extra_context(self):
return {
'hide_object': True,
'object': self.external_object,
'no_results_icon': icon_redactions,
'no_results_main_link': link_redaction_create.resolve(
context=RequestContext(
request=self.request, dict_={
'object': self.external_object
}
)
),
'no_results_text': _(
'Redactions allow removing access to confidential and '
'sensitive information without having to modify the document.'
),
'no_results_title': _('No existing redactions'),
'title': _('Redactions for: %s') % self.external_object,
}
def get_source_queryset(self):
return Redaction.objects.get_for_object(
obj=self.external_object
).filter(name__startswith='draw')

Binary file not shown.

View File

@@ -72,7 +72,8 @@ class TagsApp(MayanAppConfig):
ModelEventType.register( ModelEventType.register(
model=Tag, event_types=( model=Tag, event_types=(
event_tag_attach, event_tag_edited, event_tag_remove event_tag_attach, event_tag_created, event_tag_edited,
event_tag_remove
) )
) )

View File

@@ -61,7 +61,7 @@ class Tag(models.Model):
def get_document_count(self, user): def get_document_count(self, user):
""" """
Return the numeric count of documents that have this tag attached. Return the numeric count of documents that have this tag attached.
The count is filtered by access. The count if filtered by access.
""" """
queryset = AccessControlList.objects.restrict_queryset( queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=self.documents, permission=permission_document_view, queryset=self.documents,

View File

@@ -6,9 +6,8 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from mayan.apps.document_states.classes import WorkflowAction from mayan.apps.document_states.classes import WorkflowAction
from mayan.apps.tags.models import Tag
from .models import Tag from mayan.apps.tags.permissions import permission_tag_attach, permission_tag_remove
from .permissions import permission_tag_attach, permission_tag_remove
__all__ = ('AttachTagAction', 'RemoveTagAction') __all__ = ('AttachTagAction', 'RemoveTagAction')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -120,13 +120,11 @@ 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',
'mayan.apps.metadata', 'mayan.apps.metadata',
'mayan.apps.mirroring', 'mayan.apps.mirroring',
'mayan.apps.redactions',
'mayan.apps.ocr', 'mayan.apps.ocr',
'mayan.apps.sources', 'mayan.apps.sources',
'mayan.apps.storage', 'mayan.apps.storage',