Compare commits

..

2 Commits

Author SHA1 Message Date
Roberto Rosario
f0ae0d06c7 Switch to 2 Redis databases
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-07-02 21:40:12 -04:00
Roberto Rosario
924778d42f Update Docker image to use Python3
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-07-02 21:36:18 -04:00
81 changed files with 163 additions and 2330 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,38 +1,7 @@
Importer branch 3.2.2 (2019-06-XX)
===============
* Add a reusable task to upload documents.
* Add MVP of the importer app.
3.2.4 (2019-06-XX)
==================
* Support configurable GUnicorn timeouts. Defaults to
current value of 120 seconds.
3.2.3 (2019-06-21)
==================
* Add support for disabling the random primary key
test mixin.
* Add a reusable task to upload documents.
* Add MVP of the importer app.
* Fix mailing profile log columns mappings.
GitLab issue #626. Thanks to Jesaja Everling (@jeverling)
for the report.
* Fix the Django SMTP backend username field name.
GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
for the report and the research.
* Increase the Django STMP username.
GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
for the report and the research.
3.2.2 (2019-06-19)
================== ==================
* Fix document type change view. Closes GitLab issue #614 * Fix document type change view. Closes GitLab issue #614
Thanks to Christoph Roeder (@brightdroid) for the report. Thanks to Christoph Roeder (@brightdroid) for the report.
* Fix document parsing tool view typo. Closes GitLab issue #615.
Thanks to Tyler Page (@iamtpage) for the report.
* Update the task_check_interval_source reference
GitLab issue #617. Thanks to Lukas Gill (@lukkigi) for
the report and debug information.
3.2.1 (2019-06-14) 3.2.1 (2019-06-14)
================== ==================

View File

@@ -57,8 +57,9 @@ apt-get update \
&& echo "maxmemory-policy allkeys-lru" >> /etc/redis/redis.conf \ && echo "maxmemory-policy allkeys-lru" >> /etc/redis/redis.conf \
# Disable saving the Redis database # Disable saving the Redis database
echo "save \"\"" >> /etc/redis/redis.conf \ echo "save \"\"" >> /etc/redis/redis.conf \
# Only provision 1 database # Only provision 2 database. One for the broker and the other for
&& echo "databases 1" >> /etc/redis/redis.conf # results
&& echo "databases 2" >> /etc/redis/redis.conf
#### ####
@@ -96,14 +97,14 @@ apt-get install -y --no-install-recommends \
libssl-dev \ libssl-dev \
g++ \ g++ \
gcc \ gcc \
python-dev \ python3-dev \
python-virtualenv \ python3-virtualenv \
&& mkdir -p "${PROJECT_INSTALL_DIR}" \ && mkdir -p "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \ && chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan /src && chown -R mayan:mayan /src
USER mayan USER mayan
RUN python -m virtualenv "${PROJECT_INSTALL_DIR}" \ RUN python3 -m virtualenv -p /usr/bin/python3 "${PROJECT_INSTALL_DIR}" \
&& . "${PROJECT_INSTALL_DIR}/bin/activate" \ && . "${PROJECT_INSTALL_DIR}/bin/activate" \
&& pip install --no-cache-dir --no-use-pep517 \ && pip install --no-cache-dir --no-use-pep517 \
librabbitmq==1.6.1 \ librabbitmq==1.6.1 \

View File

@@ -22,7 +22,6 @@ export MAYAN_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE:-mayan.settings.production}
export MAYAN_GUNICORN_BIN=${MAYAN_PYTHON_BIN_DIR}gunicorn export MAYAN_GUNICORN_BIN=${MAYAN_PYTHON_BIN_DIR}gunicorn
export MAYAN_GUNICORN_WORKERS=${MAYAN_GUNICORN_WORKERS:-2} export MAYAN_GUNICORN_WORKERS=${MAYAN_GUNICORN_WORKERS:-2}
export MAYAN_GUNICORN_TIMEOUT=${MAYAN_GUNICORN_TIMEOUT:-120}
export MAYAN_PIP_BIN=${MAYAN_PYTHON_BIN_DIR}pip export MAYAN_PIP_BIN=${MAYAN_PYTHON_BIN_DIR}pip
export MAYAN_STATIC_ROOT=${MAYAN_INSTALL_DIR}/static export MAYAN_STATIC_ROOT=${MAYAN_INSTALL_DIR}/static

View File

@@ -1 +1 @@
3.2.3 3.2.1

View File

@@ -1,7 +1,7 @@
Version 3.2.2 Version 3.2.2
============= =============
Released: June 19, 2019 Released: June 17, 2019
Changes Changes
@@ -9,11 +9,6 @@ Changes
- Fix document type change view. Closes GitLab issue #614. - Fix document type change view. Closes GitLab issue #614.
Thanks to Christoph Roeder (@brightdroid) for the report. Thanks to Christoph Roeder (@brightdroid) for the report.
- Fix document parsing tool view typo. Closes GitLab issue #615.
Thanks to Tyler Page (@iamtpage) for the report.
- Update the task_check_interval_source reference
GitLab issue #617. Thanks to Lukas Gill (@lukkigi) for
the report and debug information.
Removals Removals
-------- --------
@@ -33,7 +28,7 @@ Remove deprecated requirements::
Type in the console:: Type in the console::
$ pip install mayan-edms==3.2.2 $ pip install mayan-edms==3.2.1
the requirements will also be updated automatically. the requirements will also be updated automatically.
@@ -103,7 +98,5 @@ Bugs fixed or issues closed
--------------------------- ---------------------------
- :gitlab-issue:`614` change type exception - :gitlab-issue:`614` change type exception
- :gitlab-issue:`615` TypeError: success() got an unexpected keyword argument 'requrest'
- :gitlab-issue:`617` Watcher Task not running
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/ .. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

@@ -1,113 +0,0 @@
Version 3.2.3
=============
Released: June 21, 2019
Changes
-------
- Add support for disabling the random primary key
test mixin.
- Fix mailing profile log columns mappings.
GitLab issue #626. Thanks to Jesaja Everling (@jeverling)
for the report.
- Fix the Django SMTP backend username field name.
GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
for the report and the research.
- Increase the Django STMP username.
GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
for the report and the research.
Removals
--------
- None
Upgrading from a previous version
---------------------------------
If installed via Python's PIP
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Remove deprecated requirements::
$ curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt | pip uninstall -r /dev/stdin
Type in the console::
$ pip install mayan-edms==3.2.1
the requirements will also be updated automatically.
Using Git
^^^^^^^^^
If you installed Mayan EDMS by cloning the Git repository issue the commands::
$ git reset --hard HEAD
$ git pull
otherwise download the compressed archived and uncompress it overriding the
existing installation.
Remove deprecated requirements::
$ pip uninstall -y -r removals.txt
Next upgrade/add the new requirements::
$ pip install --upgrade -r requirements.txt
Common steps
^^^^^^^^^^^^
Perform these steps after updating the code from either step above.
Make a backup of your supervisord file::
sudo cp /etc/supervisor/conf.d/mayan.conf /etc/supervisor/conf.d/mayan.conf.bck
Update the supervisord configuration file. Replace the environment
variables values show here with your respective settings. This step will refresh
the supervisord configuration file with the new queues and the latest
recommended layout::
MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
Edit the supervisord configuration file and update any setting the template
generator missed::
vi /etc/supervisor/conf.d/mayan.conf
Migrate existing database schema with::
$ mayan-edms.py performupgrade
Add new static media::
$ mayan-edms.py preparestatic --noinput
The upgrade procedure is now complete.
Backward incompatible changes
-----------------------------
- None
Bugs fixed or issues closed
---------------------------
- :gitlab-issue:`619` poplib.POP3_SSL and poplib.POP3 initialized with wrong kwarg
- :gitlab-issue:`625` mayan.apps.mailer.mailers.DjangoSMTP uses "user", but django.core.mail.backends.smtp.EmailBackend expects "username"
- :gitlab-issue:`626` Mailing profile error log is empty, despite errors
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

@@ -1,103 +0,0 @@
Version 3.2.4
=============
Released: June XX, 2019
Changes
-------
- Support configurable GUnicorn timeouts. Defaults to
current value of 120 seconds.
Removals
--------
- None
Upgrading from a previous version
---------------------------------
If installed via Python's PIP
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Remove deprecated requirements::
$ curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt | pip uninstall -r /dev/stdin
Type in the console::
$ pip install mayan-edms==3.2.3
the requirements will also be updated automatically.
Using Git
^^^^^^^^^
If you installed Mayan EDMS by cloning the Git repository issue the commands::
$ git reset --hard HEAD
$ git pull
otherwise download the compressed archived and uncompress it overriding the
existing installation.
Remove deprecated requirements::
$ pip uninstall -y -r removals.txt
Next upgrade/add the new requirements::
$ pip install --upgrade -r requirements.txt
Common steps
^^^^^^^^^^^^
Perform these steps after updating the code from either step above.
Make a backup of your supervisord file::
sudo cp /etc/supervisor/conf.d/mayan.conf /etc/supervisor/conf.d/mayan.conf.bck
Update the supervisord configuration file. Replace the environment
variables values show here with your respective settings. This step will refresh
the supervisord configuration file with the new queues and the latest
recommended layout::
MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
Edit the supervisord configuration file and update any setting the template
generator missed::
vi /etc/supervisor/conf.d/mayan.conf
Migrate existing database schema with::
$ mayan-edms.py performupgrade
Add new static media::
$ mayan-edms.py preparestatic --noinput
The upgrade procedure is now complete.
Backward incompatible changes
-----------------------------
- None
Bugs fixed or issues closed
---------------------------
- :gitlab-issue:`628` mailbox.user in POP3Email gets passed keyword argument, but only accepts "user" or positional argument
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

@@ -20,8 +20,6 @@ versions of the documentation contain the release notes for any later releases.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
3.2.4
3.2.3
3.2.2 3.2.2
3.2.1 3.2.1
3.2 3.2

View File

@@ -1,9 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
__title__ = 'Mayan EDMS' __title__ = 'Mayan EDMS'
__version__ = '3.2.3' __version__ = '3.2.1'
__build__ = 0x030203 __build__ = 0x030201
__build_string__ = 'v3.2.3_Fri Jun 21 00:01:37 2019 -0400' __build_string__ = 'v3.2.1_Fri Jun 14 03:01:40 2019 -0400'
__django_version__ = '1.11' __django_version__ = '1.11'
__author__ = 'Roberto Rosario' __author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com' __author_email__ = 'roberto.rosario@mayan-edms.com'

View File

@@ -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'
@@ -167,7 +144,6 @@ class RandomPrimaryKeyModelMonkeyPatchMixin(object):
random_primary_key_random_floor = 100 random_primary_key_random_floor = 100
random_primary_key_random_ceiling = 10000 random_primary_key_random_ceiling = 10000
random_primary_key_maximum_attempts = 100 random_primary_key_maximum_attempts = 100
random_primary_key_enable = True
@staticmethod @staticmethod
def get_unique_primary_key(model): def get_unique_primary_key(model):
@@ -194,49 +170,47 @@ class RandomPrimaryKeyModelMonkeyPatchMixin(object):
return primary_key return primary_key
def setUp(self): def setUp(self):
if self.random_primary_key_enable: self.method_save_original = models.Model.save
self.method_save_original = models.Model.save
def method_save_new(instance, *args, **kwargs): def method_save_new(instance, *args, **kwargs):
if instance.pk: if instance.pk:
return self.method_save_original(instance, *args, **kwargs) return self.method_save_original(instance, *args, **kwargs)
else: else:
# Set meta.auto_created to True to have the original save_base # Set meta.auto_created to True to have the original save_base
# not send the pre_save signal which would normally send # not send the pre_save signal which would normally send
# the instance without a primary key. Since we assign a random # the instance without a primary key. Since we assign a random
# primary key any pre_save signal handler that relies on an # primary key any pre_save signal handler that relies on an
# empty primary key will fail. # empty primary key will fail.
# The meta.auto_created and manual pre_save sending emulates # The meta.auto_created and manual pre_save sending emulates
# the original behavior. Since meta.auto_created also disables # the original behavior. Since meta.auto_created also disables
# the post_save signal we must also send it ourselves. # the post_save signal we must also send it ourselves.
# This hack work with Django 1.11 .save_base() but can break # This hack work with Django 1.11 .save_base() but can break
# in future versions if that method is updated. # in future versions if that method is updated.
pre_save.send( pre_save.send(
sender=instance.__class__, instance=instance, raw=False, sender=instance.__class__, instance=instance, raw=False,
update_fields=None, update_fields=None,
) )
instance._meta.auto_created = True instance._meta.auto_created = True
instance.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key( instance.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key(
model=instance._meta.model model=instance._meta.model
) )
instance.id = instance.pk instance.id = instance.pk
result = instance.save_base(force_insert=True) result = instance.save_base(force_insert=True)
instance._meta.auto_created = False instance._meta.auto_created = False
post_save.send( post_save.send(
sender=instance.__class__, instance=instance, created=True, sender=instance.__class__, instance=instance, created=True,
update_fields=None, raw=False update_fields=None, raw=False
) )
return result return result
setattr(models.Model, 'save', method_save_new) setattr(models.Model, 'save', method_save_new)
super(RandomPrimaryKeyModelMonkeyPatchMixin, self).setUp() super(RandomPrimaryKeyModelMonkeyPatchMixin, self).setUp()
def tearDown(self): def tearDown(self):
if self.random_primary_key_enable: models.Model.save = self.method_save_original
models.Model.save = self.method_save_original
super(RandomPrimaryKeyModelMonkeyPatchMixin, self).tearDown() super(RandomPrimaryKeyModelMonkeyPatchMixin, self).tearDown()

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

@@ -7,8 +7,7 @@ from mayan.apps.documents.tests import (
) )
from ..permissions import ( from ..permissions import (
permission_content_view, permission_document_type_parsing_setup, permission_content_view, permission_document_type_parsing_setup
permission_parse_document
) )
from ..utils import get_document_content from ..utils import get_document_content
@@ -107,43 +106,3 @@ class DocumentContentViewsTestCase(GenericDocumentViewTestCase):
response = self._request_test_document_type_parsing_settings() response = self._request_test_document_type_parsing_settings()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class DocumentContentToolsViewsTestCase(GenericDocumentViewTestCase):
_skip_file_descriptor_test = True
# Ensure we use a PDF file
test_document_filename = TEST_HYBRID_DOCUMENT
def _request_document_parsing_tool_view(self):
return self.post(
viewname='document_parsing:document_type_submit', data={
'document_type': self.test_document_type.pk
}
)
def _get_document_content(self):
return ''.join(list(get_document_content(document=self.test_document)))
def test_document_parsing_tool_view_no_permission(self):
response = self._request_document_parsing_tool_view()
self.assertEqual(response.status_code, 200)
self.assertNotContains(
response=response, status_code=200,
text=self.test_document_type.label
)
self.assertNotEqual(
self._get_document_content(), TEST_DOCUMENT_CONTENT
)
def test_document_parsing_tool_view_with_permission(self):
self.grant_permission(permission=permission_parse_document)
response = self._request_document_parsing_tool_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(
self._get_document_content(), TEST_DOCUMENT_CONTENT
)

View File

@@ -185,7 +185,7 @@ class DocumentTypeSubmitView(FormView):
'%(count)d documents added to the parsing queue.' '%(count)d documents added to the parsing queue.'
) % { ) % {
'count': count, 'count': count,
}, request=self.request }, requrest=self.request
) )
return HttpResponseRedirect(redirect_to=self.get_success_url()) return HttpResponseRedirect(redirect_to=self.get_success_url())

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

@@ -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

@@ -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

@@ -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

@@ -51,7 +51,6 @@ class MailerApp(MayanAppConfig):
LogEntry = self.get_model(model_name='LogEntry') LogEntry = self.get_model(model_name='LogEntry')
UserMailer = self.get_model(model_name='UserMailer') UserMailer = self.get_model(model_name='UserMailer')
UserMailerLogEntry = self.get_model(model_name='UserMailerLogEntry')
MailerBackend.initialize() MailerBackend.initialize()
@@ -80,13 +79,6 @@ class MailerApp(MayanAppConfig):
SourceColumn( SourceColumn(
source=UserMailer, attribute='backend_label' source=UserMailer, attribute='backend_label'
) )
SourceColumn(
attribute='datetime', label=_('Date and time'),
source=UserMailerLogEntry
)
SourceColumn(
attribute='message', label=_('Message'), source=UserMailerLogEntry
)
ModelPermission.register( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(

View File

@@ -96,7 +96,7 @@ class UserMailerDynamicForm(DynamicModelForm):
if self.instance.backend_data: if self.instance.backend_data:
backend_data = json.loads(self.instance.backend_data) backend_data = json.loads(self.instance.backend_data)
for key in self.instance.get_backend().fields: for key in self.instance.get_backend().fields:
self.fields[key].initial = backend_data.get(key) self.fields[key].initial = backend_data[key]
return result return result

View File

@@ -12,11 +12,11 @@ class DjangoSMTP(MailerBackend):
Backend that wraps Django's SMTP backend Backend that wraps Django's SMTP backend
""" """
class_fields = ( class_fields = (
'host', 'port', 'use_tls', 'use_ssl', 'username', 'password' 'host', 'port', 'use_tls', 'use_ssl', 'user', 'password'
) )
class_path = 'django.core.mail.backends.smtp.EmailBackend' class_path = 'django.core.mail.backends.smtp.EmailBackend'
field_order = ( field_order = (
'host', 'port', 'use_tls', 'use_ssl', 'username', 'password', 'from' 'host', 'port', 'use_tls', 'use_ssl', 'user', 'password', 'from'
) )
fields = { fields = {
'from': { 'from': {
@@ -60,14 +60,14 @@ class DjangoSMTP(MailerBackend):
'that "Use TLS" and "Use SSL" are mutually exclusive, ' 'that "Use TLS" and "Use SSL" are mutually exclusive, '
'so only set one of those settings to True.' 'so only set one of those settings to True.'
), 'required': False ), 'required': False
}, 'username': { }, 'user': {
'label': _('Username'), 'label': _('Username'),
'class': 'django.forms.CharField', 'default': '', 'class': 'django.forms.CharField', 'default': '',
'help_text': _( 'help_text': _(
'Username to use for the SMTP server. If empty, ' 'Username to use for the SMTP server. If empty, '
'authentication won\'t attempted.' 'authentication won\'t attempted.'
), 'kwargs': { ), 'kwargs': {
'max_length': 254 'max_length': 48
}, 'required': False }, 'required': False
}, 'password': { }, 'password': {
'label': _('Password'), 'label': _('Password'),

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

@@ -208,7 +208,7 @@ class UserMailerLogEntryListView(SingleObjectListView):
return { return {
'hide_object': True, 'hide_object': True,
'object': self.get_user_mailer(), 'object': self.get_user_mailer(),
'title': _('Error log for: %s') % self.get_user_mailer(), 'title': _('%s error log') % self.get_user_mailer(),
} }
def get_source_queryset(self): def get_source_queryset(self):

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

@@ -108,7 +108,7 @@ class PlatformTemplate(object):
class PlatformTemplateSupervisord(PlatformTemplate): class PlatformTemplateSupervisord(PlatformTemplate):
context_defaults = { context_defaults = {
'BROKER_URL': 'redis://127.0.0.1:6379/0', 'BROKER_URL': 'redis://127.0.0.1:6379/0',
'CELERY_RESULT_BACKEND': 'redis://127.0.0.1:6379/0', 'CELERY_RESULT_BACKEND': 'redis://127.0.0.1:6379/1',
} }
label = _('Template for Supervisord.') label = _('Template for Supervisord.')
name = 'supervisord' name = 'supervisord'
@@ -120,10 +120,6 @@ class PlatformTemplateSupervisord(PlatformTemplate):
name='GUNICORN_WORKERS', default=2, name='GUNICORN_WORKERS', default=2,
environment_name='MAYAN_GUNICORN_WORKERS' environment_name='MAYAN_GUNICORN_WORKERS'
), ),
Variable(
name='GUNICORN_TIMEOUT', default=120,
environment_name='MAYAN_GUNICORN_TIMEOUT'
),
Variable( Variable(
name='DATABASE_CONN_MAX_AGE', default=0, name='DATABASE_CONN_MAX_AGE', default=0,
environment_name='MAYAN_DATABASE_CONN_MAX_AGE' environment_name='MAYAN_DATABASE_CONN_MAX_AGE'

View File

@@ -16,7 +16,7 @@ environment=
[program:mayan-gunicorn] [program:mayan-gunicorn]
autorestart = true autorestart = true
autostart = true autostart = true
command = {{ INSTALLATION_PATH }}/bin/gunicorn -w {{ GUNICORN_WORKERS }} mayan.wsgi --max-requests 500 --max-requests-jitter 50 --worker-class gevent --bind 0.0.0.0:8000 --timeout {{ GUNICORN_TIMEOUT }} command = {{ INSTALLATION_PATH }}/bin/gunicorn -w {{ GUNICORN_WORKERS }} mayan.wsgi --max-requests 500 --max-requests-jitter 50 --worker-class gevent --bind 0.0.0.0:8000 --timeout 120
user = mayan user = mayan
{% for worker in workers %} {% for worker in workers %}

View File

@@ -1,7 +1,7 @@
[program:mayan-gunicorn] [program:mayan-gunicorn]
autorestart = false autorestart = false
autostart = true autostart = true
command = /bin/bash -c "${MAYAN_GUNICORN_BIN} -w ${MAYAN_GUNICORN_WORKERS} mayan.wsgi --max-requests 500 --max-requests-jitter 50 --worker-class gevent --bind 0.0.0.0:8000 --env DJANGO_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE}" --timeout ${MAYAN_GUNICORN_TIMEOUT} command = /bin/bash -c "${MAYAN_GUNICORN_BIN} -w ${MAYAN_GUNICORN_WORKERS} mayan.wsgi --max-requests 500 --max-requests-jitter 50 --worker-class gevent --bind 0.0.0.0:8000 --env DJANGO_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE}" --timeout 120
redirect_stderr = true redirect_stderr = true
stderr_logfile = /dev/fd/2 stderr_logfile = /dev/fd/2
stderr_logfile_maxbytes = 0 stderr_logfile_maxbytes = 0

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')

View File

@@ -234,7 +234,7 @@ class IntervalBaseModel(OutOfProcessSource):
PeriodicTask.objects.create( PeriodicTask.objects.create(
name=self._get_periodic_task_name(), name=self._get_periodic_task_name(),
interval=interval_instance, interval=interval_instance,
task='mayan.apps.sources.tasks.task_check_interval_source', task='sources.tasks.task_check_interval_source',
kwargs=json.dumps({'source_id': self.pk}) kwargs=json.dumps({'source_id': self.pk})
) )

View File

@@ -277,15 +277,15 @@ class POP3Email(EmailBaseModel):
logger.debug('ssl: %s', self.ssl) logger.debug('ssl: %s', self.ssl)
if self.ssl: if self.ssl:
mailbox = poplib.POP3_SSL(host=self.host, port=self.port) mailbox = poplib.POP3_SSL(host=self.host, post=self.port)
else: else:
mailbox = poplib.POP3( mailbox = poplib.POP3(
host=self.host, port=self.port, timeout=self.timeout host=self.host, post=self.port, timeout=self.timeout
) )
mailbox.getwelcome() mailbox.getwelcome()
mailbox.user(self.username) mailbox.user(username=self.username)
mailbox.pass_(self.password) mailbox.pass_(password=self.password)
messages_info = mailbox.list() messages_info = mailbox.list()
logger.debug(msg='messages_info:') logger.debug(msg='messages_info:')

View File

@@ -203,10 +203,7 @@ class POP3SourceTestCase(GenericDocumentTestCase):
def list(self, which=None): def list(self, which=None):
return (None, ['1 test']) return (None, ['1 test'])
def user(self, user): def pass_(self, password):
return
def pass_(self, pswd):
return return
def quit(self): def quit(self):
@@ -217,7 +214,10 @@ class POP3SourceTestCase(GenericDocumentTestCase):
1, [TEST_EMAIL_BASE64_FILENAME] 1, [TEST_EMAIL_BASE64_FILENAME]
) )
@mock.patch('poplib.POP3_SSL', autospec=True) def user(self, username):
return
@mock.patch('poplib.POP3_SSL')
def test_download_document(self, mock_poplib): def test_download_document(self, mock_poplib):
mock_poplib.return_value = POP3SourceTestCase.MockMailbox() mock_poplib.return_value = POP3SourceTestCase.MockMailbox()
self.source = POP3Email.objects.create( self.source = POP3Email.objects.create(

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',