PEP8 cleanups, specially E501 line too long.
This commit is contained in:
@@ -15,5 +15,8 @@ class ACLsApp(MayanAppConfig):
|
||||
def ready(self):
|
||||
super(ACLsApp, self).ready()
|
||||
|
||||
menu_object.bind_links(links=[link_acl_permissions, link_acl_delete], sources=[AccessControlList])
|
||||
menu_object.bind_links(
|
||||
links=[link_acl_permissions, link_acl_delete],
|
||||
sources=[AccessControlList]
|
||||
)
|
||||
menu_sidebar.bind_links(links=[link_acl_new], sources=['acls:acl_list'])
|
||||
|
||||
@@ -16,7 +16,19 @@ def get_kwargs_factory(variable_name):
|
||||
return get_kwargs
|
||||
|
||||
|
||||
link_acl_delete = Link(permissions=[permission_acl_edit], tags='dangerous', text=_('Delete'), view='acls:acl_delete', args='resolved_object.pk')
|
||||
link_acl_list = Link(permissions=[permission_acl_view], text=_('ACLs'), view='acls:acl_list', kwargs=get_kwargs_factory('resolved_object'))
|
||||
link_acl_new = Link(permissions=[permission_acl_edit], text=_('New ACL'), view='acls:acl_new', kwargs=get_kwargs_factory('resolved_object'))
|
||||
link_acl_permissions = Link(permissions=[permission_acl_edit], text=_('Permissions'), view='acls:acl_permissions', args='resolved_object.pk')
|
||||
link_acl_delete = Link(
|
||||
permissions=[permission_acl_edit], tags='dangerous', text=_('Delete'),
|
||||
view='acls:acl_delete', args='resolved_object.pk'
|
||||
)
|
||||
link_acl_list = Link(
|
||||
permissions=[permission_acl_view], text=_('ACLs'), view='acls:acl_list',
|
||||
kwargs=get_kwargs_factory('resolved_object')
|
||||
)
|
||||
link_acl_new = Link(
|
||||
permissions=[permission_acl_edit], text=_('New ACL'),
|
||||
view='acls:acl_new', kwargs=get_kwargs_factory('resolved_object')
|
||||
)
|
||||
link_acl_permissions = Link(
|
||||
permissions=[permission_acl_edit], text=_('Permissions'),
|
||||
view='acls:acl_permissions', args='resolved_object.pk'
|
||||
)
|
||||
|
||||
@@ -38,7 +38,10 @@ class AccessControlListManager(models.Manager):
|
||||
parent_object = getattr(instance, parent_accessor)
|
||||
content_type = ContentType.objects.get_for_model(parent_object)
|
||||
try:
|
||||
return self.get(role=role, content_type=content_type, object_id=parent_object.pk).permissions.all()
|
||||
return self.get(
|
||||
role=role, content_type=content_type,
|
||||
object_id=parent_object.pk
|
||||
).permissions.all()
|
||||
except self.model.DoesNotExist:
|
||||
return StoredPermission.objects.none()
|
||||
|
||||
@@ -47,7 +50,9 @@ class AccessControlListManager(models.Manager):
|
||||
return True
|
||||
|
||||
try:
|
||||
stored_permissions = [permission.stored_permission for permission in permissions]
|
||||
stored_permissions = [
|
||||
permission.stored_permission for permission in permissions
|
||||
]
|
||||
except TypeError:
|
||||
stored_permissions = [permissions.stored_permission]
|
||||
|
||||
@@ -80,14 +85,22 @@ class AccessControlListManager(models.Manager):
|
||||
instance = queryset.first()
|
||||
if instance:
|
||||
parent_object = getattr(instance, parent_accessor)
|
||||
parent_content_type = ContentType.objects.get_for_model(parent_object)
|
||||
parent_queryset = self.filter(content_type=parent_content_type, role__in=user_roles, permissions=permission.stored_permission)
|
||||
parent_content_type = ContentType.objects.get_for_model(
|
||||
parent_object
|
||||
)
|
||||
parent_queryset = self.filter(
|
||||
content_type=parent_content_type, role__in=user_roles,
|
||||
permissions=permission.stored_permission
|
||||
)
|
||||
parent_acl_query = Q(**{'{}__pk__in'.format(parent_accessor): parent_queryset.values_list('object_id', flat=True)})
|
||||
else:
|
||||
parent_acl_query = Q()
|
||||
|
||||
# Directly granted access
|
||||
content_type = ContentType.objects.get_for_model(queryset.model)
|
||||
acl_query = Q(pk__in=self.filter(content_type=content_type, role__in=user_roles, permissions=permission.stored_permission).values_list('object_id', flat=True))
|
||||
acl_query = Q(pk__in=self.filter(
|
||||
content_type=content_type, role__in=user_roles,
|
||||
permissions=permission.stored_permission
|
||||
).values_list('object_id', flat=True))
|
||||
|
||||
return queryset.filter(parent_acl_query | acl_query)
|
||||
|
||||
@@ -22,5 +22,11 @@ class AuthenticationApp(MayanAppConfig):
|
||||
links=[
|
||||
link_password_change, link_logout
|
||||
],
|
||||
sources=['common:current_user_details', 'common:current_user_edit', 'common:current_user_locale_profile_details', 'common:current_user_locale_profile_edit', 'authentication:password_change_view', 'common:setup_list', 'common:tools_list']
|
||||
sources=[
|
||||
'common:current_user_details', 'common:current_user_edit',
|
||||
'common:current_user_locale_profile_details',
|
||||
'common:current_user_locale_profile_edit',
|
||||
'authentication:password_change_view',
|
||||
'common:setup_list', 'common:tools_list'
|
||||
]
|
||||
)
|
||||
|
||||
@@ -9,5 +9,10 @@ def has_usable_password(context):
|
||||
return context['request'].user.has_usable_password
|
||||
|
||||
|
||||
link_logout = Link(icon='fa fa-sign-out', text=_('Logout'), view='authentication:logout_view')
|
||||
link_password_change = Link(condition=has_usable_password, icon='fa fa-key', text=_('Change password'), view='authentication:password_change_view')
|
||||
link_logout = Link(
|
||||
icon='fa fa-sign-out', text=_('Logout'), view='authentication:logout_view'
|
||||
)
|
||||
link_password_change = Link(
|
||||
condition=has_usable_password, icon='fa fa-key', text=_('Change password'),
|
||||
view='authentication:password_change_view'
|
||||
)
|
||||
|
||||
@@ -5,4 +5,9 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from smart_settings import Namespace
|
||||
|
||||
namespace = Namespace(name='authentication', label=_('Authentication'))
|
||||
setting_login_method = namespace.add_setting(global_name='AUTHENTICATION_LOGIN_METHOD', default='username', help_text=_('Controls the mechanism used to authenticated user. Options are: username, email'))
|
||||
setting_login_method = namespace.add_setting(
|
||||
global_name='AUTHENTICATION_LOGIN_METHOD', default='username',
|
||||
help_text=_(
|
||||
'Controls the mechanism used to authenticated user. Options are: username, email'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -19,17 +19,24 @@ class UserLoginTestCase(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.admin_user = User.objects.create_superuser(username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD)
|
||||
self.admin_user = User.objects.create_superuser(
|
||||
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
|
||||
password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
self.client = Client()
|
||||
|
||||
def test_normal_behaviour(self):
|
||||
setting_login_method.value = 'username'
|
||||
response = self.client.get(reverse('documents:document_list'))
|
||||
self.assertRedirects(response, 'http://testserver/authentication/login/')
|
||||
self.assertRedirects(
|
||||
response, 'http://testserver/authentication/login/'
|
||||
)
|
||||
|
||||
def test_username_login(self):
|
||||
setting_login_method.value = 'username'
|
||||
logged_in = self.client.login(username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD)
|
||||
logged_in = self.client.login(
|
||||
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
self.assertTrue(logged_in)
|
||||
response = self.client.get(reverse('documents:document_list'))
|
||||
# We didn't get redirected to the login URL
|
||||
@@ -39,10 +46,14 @@ class UserLoginTestCase(TestCase):
|
||||
with self.settings(AUTHENTICATION_BACKENDS=('authentication.auth.email_auth_backend.EmailAuthBackend',)):
|
||||
setting_login_method.value = 'email'
|
||||
|
||||
logged_in = self.client.login(username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD)
|
||||
logged_in = self.client.login(
|
||||
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
self.assertFalse(logged_in)
|
||||
|
||||
logged_in = self.client.login(email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD)
|
||||
logged_in = self.client.login(
|
||||
email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
self.assertTrue(logged_in)
|
||||
|
||||
response = self.client.get(reverse('documents:document_list'))
|
||||
@@ -52,9 +63,16 @@ class UserLoginTestCase(TestCase):
|
||||
def test_username_login_via_views(self):
|
||||
setting_login_method.value = 'username'
|
||||
response = self.client.get(reverse('documents:document_list'))
|
||||
self.assertRedirects(response, 'http://testserver/authentication/login/')
|
||||
self.assertRedirects(
|
||||
response, 'http://testserver/authentication/login/'
|
||||
)
|
||||
|
||||
response = self.client.post(reverse(settings.LOGIN_URL), {'username': TEST_ADMIN_USERNAME, 'password': TEST_ADMIN_PASSWORD})
|
||||
response = self.client.post(
|
||||
reverse(settings.LOGIN_URL), {
|
||||
'username': TEST_ADMIN_USERNAME,
|
||||
'password': TEST_ADMIN_PASSWORD
|
||||
}
|
||||
)
|
||||
response = self.client.get(reverse('documents:document_list'))
|
||||
# We didn't get redirected to the login URL
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -63,9 +81,15 @@ class UserLoginTestCase(TestCase):
|
||||
with self.settings(AUTHENTICATION_BACKENDS=('authentication.auth.email_auth_backend.EmailAuthBackend',)):
|
||||
setting_login_method.value = 'email'
|
||||
response = self.client.get(reverse('documents:document_list'))
|
||||
self.assertRedirects(response, 'http://testserver/authentication/login/')
|
||||
self.assertRedirects(
|
||||
response, 'http://testserver/authentication/login/'
|
||||
)
|
||||
|
||||
response = self.client.post(reverse(settings.LOGIN_URL), {'email': TEST_ADMIN_EMAIL, 'password': TEST_ADMIN_PASSWORD}, follow=True)
|
||||
response = self.client.post(
|
||||
reverse(settings.LOGIN_URL), {
|
||||
'email': TEST_ADMIN_EMAIL, 'password': TEST_ADMIN_PASSWORD
|
||||
}, follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse('documents:document_list'))
|
||||
|
||||
@@ -5,16 +5,46 @@ from django.conf.urls import patterns, url
|
||||
|
||||
urlpatterns = patterns(
|
||||
'authentication.views',
|
||||
url(r'^login/$', 'login_view', (), name='login_view'),
|
||||
url(r'^password/change/done/$', 'password_change_done', (), name='password_change_done'),
|
||||
url(r'^password/change/$', 'password_change_view', (), name='password_change_view'),
|
||||
url(r'^login/$', 'login_view', name='login_view'),
|
||||
url(
|
||||
r'^password/change/done/$', 'password_change_done',
|
||||
name='password_change_done'
|
||||
),
|
||||
url(
|
||||
r'^password/change/$', 'password_change_view',
|
||||
name='password_change_view'
|
||||
),
|
||||
)
|
||||
|
||||
urlpatterns += patterns(
|
||||
'',
|
||||
url(r'^logout/$', 'django.contrib.auth.views.logout', {'next_page': settings.LOGIN_REDIRECT_URL}, name='logout_view'),
|
||||
url(r'^password/reset/$', 'django.contrib.auth.views.password_reset', {'email_template_name': 'appearance/password_reset_email.html', 'template_name': 'appearance/password_reset_form.html', 'post_reset_redirect': '/password/reset/done'}, name='password_reset_view'),
|
||||
url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm', {'template_name': 'appearance/password_reset_confirm.html', 'post_reset_redirect': '/password/reset/complete/'}, name='password_reset_confirm_view'),
|
||||
url(r'^password/reset/complete/$', 'django.contrib.auth.views.password_reset_complete', {'template_name': 'appearance/password_reset_complete.html'}, name='password_reset_complete_view'),
|
||||
url(r'^password/reset/done/$', 'django.contrib.auth.views.password_reset_done', {'template_name': 'appearance/password_reset_done.html'}, name='password_reset_done_view'),
|
||||
url(
|
||||
r'^logout/$', 'django.contrib.auth.views.logout',
|
||||
{'next_page': settings.LOGIN_REDIRECT_URL}, name='logout_view'
|
||||
),
|
||||
url(
|
||||
r'^password/reset/$', 'django.contrib.auth.views.password_reset',
|
||||
{
|
||||
'email_template_name': 'appearance/password_reset_email.html',
|
||||
'template_name': 'appearance/password_reset_form.html',
|
||||
'post_reset_redirect': '/password/reset/done'
|
||||
}, name='password_reset_view'
|
||||
),
|
||||
url(
|
||||
r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
|
||||
'django.contrib.auth.views.password_reset_confirm', {
|
||||
'template_name': 'appearance/password_reset_confirm.html',
|
||||
'post_reset_redirect': '/password/reset/complete/'
|
||||
}, name='password_reset_confirm_view'
|
||||
),
|
||||
url(
|
||||
r'^password/reset/complete/$',
|
||||
'django.contrib.auth.views.password_reset_complete', {
|
||||
'template_name': 'appearance/password_reset_complete.html'
|
||||
}, name='password_reset_complete_view'),
|
||||
url(
|
||||
r'^password/reset/done/$',
|
||||
'django.contrib.auth.views.password_reset_done', {
|
||||
'template_name': 'appearance/password_reset_done.html'
|
||||
}, name='password_reset_done_view'),
|
||||
)
|
||||
|
||||
@@ -48,5 +48,7 @@ def password_change_done(request):
|
||||
View called when the new user password has been accepted
|
||||
"""
|
||||
|
||||
messages.success(request, _('Your password has been successfully changed.'))
|
||||
messages.success(
|
||||
request, _('Your password has been successfully changed.')
|
||||
)
|
||||
return redirect('common:current_user_details')
|
||||
|
||||
@@ -37,10 +37,22 @@ class CheckoutsApp(MayanAppConfig):
|
||||
|
||||
APIEndPoint('checkouts')
|
||||
|
||||
Document.add_to_class('check_in', lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user))
|
||||
Document.add_to_class('checkout_info', lambda document: DocumentCheckout.objects.document_checkout_info(document))
|
||||
Document.add_to_class('checkout_state', lambda document: DocumentCheckout.objects.document_checkout_state(document))
|
||||
Document.add_to_class('is_checked_out', lambda document: DocumentCheckout.objects.is_document_checked_out(document))
|
||||
Document.add_to_class(
|
||||
'check_in',
|
||||
lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user)
|
||||
)
|
||||
Document.add_to_class(
|
||||
'checkout_info',
|
||||
lambda document: DocumentCheckout.objects.document_checkout_info(document)
|
||||
)
|
||||
Document.add_to_class(
|
||||
'checkout_state',
|
||||
lambda document: DocumentCheckout.objects.document_checkout_state(document)
|
||||
)
|
||||
Document.add_to_class(
|
||||
'is_checked_out',
|
||||
lambda document: DocumentCheckout.objects.is_document_checked_out(document)
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=Document, permissions=(
|
||||
@@ -54,13 +66,18 @@ class CheckoutsApp(MayanAppConfig):
|
||||
{
|
||||
'task_check_expired_check_outs': {
|
||||
'task': 'checkouts.tasks.task_check_expired_check_outs',
|
||||
'schedule': timedelta(seconds=CHECK_EXPIRED_CHECK_OUTS_INTERVAL),
|
||||
'schedule': timedelta(
|
||||
seconds=CHECK_EXPIRED_CHECK_OUTS_INTERVAL
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
app.conf.CELERY_QUEUES.append(
|
||||
Queue('checkouts_periodic', Exchange('checkouts_periodic'), routing_key='checkouts_periodic', delivery_mode=1),
|
||||
Queue(
|
||||
'checkouts_periodic', Exchange('checkouts_periodic'),
|
||||
routing_key='checkouts_periodic', delivery_mode=1
|
||||
),
|
||||
)
|
||||
|
||||
app.conf.CELERY_ROUTES.update(
|
||||
@@ -73,6 +90,15 @@ class CheckoutsApp(MayanAppConfig):
|
||||
|
||||
menu_facet.bind_links(links=[link_checkout_info], sources=[Document])
|
||||
menu_main.bind_links(links=[link_checkout_list])
|
||||
menu_sidebar.bind_links(links=[link_checkout_document, link_checkin_document], sources=['checkouts:checkout_info', 'checkouts:checkout_document', 'checkouts:checkin_document'])
|
||||
menu_sidebar.bind_links(
|
||||
links=[link_checkout_document, link_checkin_document],
|
||||
sources=[
|
||||
'checkouts:checkout_info', 'checkouts:checkout_document',
|
||||
'checkouts:checkin_document'
|
||||
]
|
||||
)
|
||||
|
||||
pre_save.connect(check_if_new_versions_allowed, dispatch_uid='document_index_delete', sender=DocumentVersion)
|
||||
pre_save.connect(
|
||||
check_if_new_versions_allowed,
|
||||
dispatch_uid='document_index_delete', sender=DocumentVersion
|
||||
)
|
||||
|
||||
@@ -4,7 +4,17 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from events.classes import Event
|
||||
|
||||
event_document_auto_check_in = Event(name='checkouts_document_auto_check_in', label=_('Document automatically checked in'))
|
||||
event_document_check_in = Event(name='checkouts_document_check_in', label=_('Document checked in'))
|
||||
event_document_check_out = Event(name='checkouts_document_check_out', label=_('Document checked out'))
|
||||
event_document_forceful_check_in = Event(name='checkouts_document_forceful_check_in', label=_('Document forcefully checked in'))
|
||||
event_document_auto_check_in = Event(
|
||||
name='checkouts_document_auto_check_in',
|
||||
label=_('Document automatically checked in')
|
||||
)
|
||||
event_document_check_in = Event(
|
||||
name='checkouts_document_check_in', label=_('Document checked in')
|
||||
)
|
||||
event_document_check_out = Event(
|
||||
name='checkouts_document_check_out', label=_('Document checked out')
|
||||
)
|
||||
event_document_forceful_check_in = Event(
|
||||
name='checkouts_document_forceful_check_in',
|
||||
label=_('Document forcefully checked in')
|
||||
)
|
||||
|
||||
@@ -8,4 +8,8 @@ from .models import DocumentCheckout
|
||||
|
||||
def check_if_new_versions_allowed(sender, **kwargs):
|
||||
if not DocumentCheckout.objects.are_document_new_versions_allowed(kwargs['instance'].document):
|
||||
raise NewDocumentVersionNotAllowed(_('New versions not allowed for the checkedout document: %s' % kwargs['instance'].document))
|
||||
raise NewDocumentVersionNotAllowed(
|
||||
_(
|
||||
'New versions not allowed for the checkedout document: %s' % kwargs['instance'].document
|
||||
)
|
||||
)
|
||||
|
||||
@@ -18,7 +18,24 @@ def is_not_checked_out(context):
|
||||
return not context['object'].is_checked_out()
|
||||
|
||||
|
||||
link_checkout_list = Link(icon='fa fa-shopping-cart', text=_('Checkouts'), view='checkouts:checkout_list')
|
||||
link_checkout_document = Link(condition=is_not_checked_out, permissions=[permission_document_checkout], text=_('Check out document'), view='checkouts:checkout_document', args='object.pk')
|
||||
link_checkin_document = Link(condition=is_checked_out, permissions=[permission_document_checkin, permission_document_checkin_override], text=_('Check in document'), view='checkouts:checkin_document', args='object.pk')
|
||||
link_checkout_info = Link(permissions=[permission_document_checkin, permission_document_checkin_override, permission_document_checkout], text=_('Check in/out'), view='checkouts:checkout_info', args='object.pk')
|
||||
link_checkout_list = Link(
|
||||
icon='fa fa-shopping-cart', text=_('Checkouts'),
|
||||
view='checkouts:checkout_list'
|
||||
)
|
||||
link_checkout_document = Link(
|
||||
condition=is_not_checked_out, permissions=[permission_document_checkout],
|
||||
text=_('Check out document'), view='checkouts:checkout_document',
|
||||
args='object.pk'
|
||||
)
|
||||
link_checkin_document = Link(
|
||||
condition=is_checked_out, permissions=[
|
||||
permission_document_checkin, permission_document_checkin_override
|
||||
], text=_('Check in document'), view='checkouts:checkin_document',
|
||||
args='object.pk'
|
||||
)
|
||||
link_checkout_info = Link(
|
||||
permissions=[
|
||||
permission_document_checkin, permission_document_checkin_override,
|
||||
permission_document_checkout
|
||||
], text=_('Check in/out'), view='checkouts:checkout_info', args='object.pk'
|
||||
)
|
||||
|
||||
@@ -19,13 +19,24 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class DocumentCheckoutManager(models.Manager):
|
||||
def checkout_document(self, document, expiration_datetime, user, block_new_version=True):
|
||||
self.create(document=document, expiration_datetime=expiration_datetime, user=user, block_new_version=block_new_version)
|
||||
self.create(
|
||||
document=document, expiration_datetime=expiration_datetime,
|
||||
user=user, block_new_version=block_new_version
|
||||
)
|
||||
|
||||
def checked_out_documents(self):
|
||||
return Document.objects.filter(pk__in=self.model.objects.all().values_list('document__pk', flat=True))
|
||||
return Document.objects.filter(
|
||||
pk__in=self.model.objects.all().values_list(
|
||||
'document__pk', flat=True
|
||||
)
|
||||
)
|
||||
|
||||
def expired_check_outs(self):
|
||||
expired_list = Document.objects.filter(pk__in=self.model.objects.filter(expiration_datetime__lte=now()).values_list('document__pk', flat=True))
|
||||
expired_list = Document.objects.filter(
|
||||
pk__in=self.model.objects.filter(
|
||||
expiration_datetime__lte=now()
|
||||
).values_list('document__pk', flat=True)
|
||||
)
|
||||
logger.debug('expired_list: %s', expired_list)
|
||||
return expired_list
|
||||
|
||||
@@ -47,7 +58,9 @@ class DocumentCheckoutManager(models.Manager):
|
||||
else:
|
||||
if user:
|
||||
if self.document_checkout_info(document).user != user:
|
||||
event_document_forceful_check_in.commit(actor=user, target=document)
|
||||
event_document_forceful_check_in.commit(
|
||||
actor=user, target=document
|
||||
)
|
||||
else:
|
||||
event_document_check_in.commit(actor=user, target=document)
|
||||
else:
|
||||
|
||||
@@ -24,11 +24,26 @@ class DocumentCheckout(models.Model):
|
||||
"""
|
||||
Model to store the state and information of a document checkout
|
||||
"""
|
||||
document = models.ForeignKey(Document, unique=True, verbose_name=_('Document'))
|
||||
checkout_datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Check out date and time'))
|
||||
expiration_datetime = models.DateTimeField(help_text=_('Amount of time to hold the document checked out in minutes.'), verbose_name=_('Check out expiration date and time'))
|
||||
document = models.ForeignKey(
|
||||
Document, unique=True, verbose_name=_('Document')
|
||||
)
|
||||
checkout_datetime = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name=_('Check out date and time')
|
||||
)
|
||||
expiration_datetime = models.DateTimeField(
|
||||
help_text=_(
|
||||
'Amount of time to hold the document checked out in minutes.'
|
||||
),
|
||||
verbose_name=_('Check out expiration date and time')
|
||||
)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'))
|
||||
block_new_version = models.BooleanField(default=True, help_text=_('Do not allow new version of this document to be uploaded.'), verbose_name=_('Block new version upload'))
|
||||
block_new_version = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_(
|
||||
'Do not allow new version of this document to be uploaded.'
|
||||
),
|
||||
verbose_name=_('Block new version upload')
|
||||
)
|
||||
|
||||
objects = DocumentCheckoutManager()
|
||||
|
||||
@@ -40,7 +55,9 @@ class DocumentCheckout(models.Model):
|
||||
|
||||
def clean(self):
|
||||
if self.expiration_datetime < now():
|
||||
raise ValidationError(_('Check out expiration date and time must be in the future.'))
|
||||
raise ValidationError(
|
||||
_('Check out expiration date and time must be in the future.')
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
new_checkout = not self.pk
|
||||
@@ -49,8 +66,13 @@ class DocumentCheckout(models.Model):
|
||||
|
||||
result = super(DocumentCheckout, self).save(*args, **kwargs)
|
||||
if new_checkout:
|
||||
event_document_check_out.commit(actor=self.user, target=self.document)
|
||||
logger.info('Document "%s" checked out by user "%s"', self.document, self.user)
|
||||
event_document_check_out.commit(
|
||||
actor=self.user, target=self.document
|
||||
)
|
||||
logger.info(
|
||||
'Document "%s" checked out by user "%s"',
|
||||
self.document, self.user
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@ from permissions import PermissionNamespace
|
||||
|
||||
namespace = PermissionNamespace('checkouts', _('Document checkout'))
|
||||
|
||||
permission_document_checkin = namespace.add_permission(name='checkin_document', label=_('Check in documents'))
|
||||
permission_document_checkin_override = namespace.add_permission(name='checkin_document_override', label=_('Forcefully check in documents'))
|
||||
permission_document_checkout = namespace.add_permission(name='checkout_document', label=_('Check out documents'))
|
||||
permission_document_checkin = namespace.add_permission(
|
||||
name='checkin_document', label=_('Check in documents')
|
||||
)
|
||||
permission_document_checkin_override = namespace.add_permission(
|
||||
name='checkin_document_override', label=_('Forcefully check in documents')
|
||||
)
|
||||
permission_document_checkout = namespace.add_permission(
|
||||
name='checkout_document', label=_('Check out documents')
|
||||
)
|
||||
|
||||
@@ -17,10 +17,11 @@ def task_check_expired_check_outs():
|
||||
lock_id = 'task_expired_check_outs'
|
||||
try:
|
||||
logger.debug('trying to acquire lock: %s', lock_id)
|
||||
lock = Lock.acquire_lock(name=lock_id, timeout=CHECKOUT_EXPIRATION_LOCK_EXPIRE)
|
||||
lock = Lock.acquire_lock(
|
||||
name=lock_id, timeout=CHECKOUT_EXPIRATION_LOCK_EXPIRE
|
||||
)
|
||||
logger.debug('acquired lock: %s', lock_id)
|
||||
DocumentCheckout.objects.check_in_expired_check_outs()
|
||||
lock.release()
|
||||
except LockError:
|
||||
logger.debug('unable to obtain lock')
|
||||
pass
|
||||
|
||||
@@ -8,13 +8,28 @@ from .views import CheckoutDocumentView, CheckoutListView
|
||||
urlpatterns = patterns(
|
||||
'checkouts.views',
|
||||
url(r'^list/$', CheckoutListView.as_view(), name='checkout_list'),
|
||||
url(r'^(?P<pk>\d+)/check/out/$', CheckoutDocumentView.as_view(), name='checkout_document'),
|
||||
url(r'^(?P<document_pk>\d+)/check/in/$', 'checkin_document', name='checkin_document'),
|
||||
url(r'^(?P<document_pk>\d+)/check/info/$', 'checkout_info', name='checkout_info'),
|
||||
url(
|
||||
r'^(?P<pk>\d+)/check/out/$', CheckoutDocumentView.as_view(),
|
||||
name='checkout_document'
|
||||
),
|
||||
url(
|
||||
r'^(?P<document_pk>\d+)/check/in/$', 'checkin_document',
|
||||
name='checkin_document'
|
||||
),
|
||||
url(
|
||||
r'^(?P<document_pk>\d+)/check/info/$', 'checkout_info',
|
||||
name='checkout_info'
|
||||
),
|
||||
)
|
||||
|
||||
api_urls = patterns(
|
||||
'',
|
||||
url(r'^documents/$', APICheckedoutDocumentListView.as_view(), name='checkout-document-list'),
|
||||
url(r'^documents/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(), name='checkedout-document-view'),
|
||||
url(
|
||||
r'^documents/$', APICheckedoutDocumentListView.as_view(),
|
||||
name='checkout-document-list'
|
||||
),
|
||||
url(
|
||||
r'^documents/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(),
|
||||
name='checkedout-document-view'
|
||||
),
|
||||
)
|
||||
|
||||
@@ -34,9 +34,13 @@ class CheckoutDocumentView(SingleObjectCreateView):
|
||||
self.document = get_object_or_404(Document, pk=self.kwargs['pk'])
|
||||
|
||||
try:
|
||||
Permission.check_permissions(request.user, [permission_document_checkout])
|
||||
Permission.check_permissions(
|
||||
request.user, [permission_document_checkout]
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(permission_document_checkout, request.user, self.document)
|
||||
AccessControlList.objects.check_access(
|
||||
permission_document_checkout, request.user, self.document
|
||||
)
|
||||
|
||||
return super(CheckoutDocumentView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -49,9 +53,15 @@ class CheckoutDocumentView(SingleObjectCreateView):
|
||||
except DocumentAlreadyCheckedOut:
|
||||
messages.error(self.request, _('Document already checked out.'))
|
||||
except Exception as exception:
|
||||
messages.error(self.request, _('Error trying to check out document; %s') % exception)
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Error trying to check out document; %s') % exception
|
||||
)
|
||||
else:
|
||||
messages.success(self.request, _('Document "%s" checked out successfully.') % self.document)
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Document "%s" checked out successfully.') % self.document
|
||||
)
|
||||
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
@@ -73,9 +83,24 @@ class CheckoutListView(DocumentListView):
|
||||
'title': _('Documents checked out'),
|
||||
'hide_links': True,
|
||||
'extra_columns': [
|
||||
{'name': _('User'), 'attribute': encapsulate(lambda document: document.checkout_info().user.get_full_name() or document.checkout_info().user)},
|
||||
{'name': _('Checkout time and date'), 'attribute': encapsulate(lambda document: document.checkout_info().checkout_datetime)},
|
||||
{'name': _('Checkout expiration'), 'attribute': encapsulate(lambda document: document.checkout_info().expiration_datetime)},
|
||||
{
|
||||
'name': _('User'),
|
||||
'attribute': encapsulate(
|
||||
lambda document: document.checkout_info().user.get_full_name() or document.checkout_info().user
|
||||
)
|
||||
},
|
||||
{
|
||||
'name': _('Checkout time and date'),
|
||||
'attribute': encapsulate(
|
||||
lambda document: document.checkout_info().checkout_datetime
|
||||
)
|
||||
},
|
||||
{
|
||||
'name': _('Checkout expiration'),
|
||||
'attribute': encapsulate(
|
||||
lambda document: document.checkout_info().expiration_datetime
|
||||
)
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -83,29 +108,51 @@ class CheckoutListView(DocumentListView):
|
||||
def checkout_info(request, document_pk):
|
||||
document = get_object_or_404(Document, pk=document_pk)
|
||||
try:
|
||||
Permission.check_permissions(request.user, [permission_document_checkout, permission_document_checkin])
|
||||
Permission.check_permissions(
|
||||
request.user, [
|
||||
permission_document_checkout, permission_document_checkin
|
||||
]
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access([permission_document_checkout, permission_document_checkin], request.user, document)
|
||||
AccessControlList.objects.check_access(
|
||||
[permission_document_checkout, permission_document_checkin],
|
||||
request.user, document
|
||||
)
|
||||
|
||||
paragraphs = [_('Document status: %s') % STATE_LABELS[document.checkout_state()]]
|
||||
paragraphs = [
|
||||
_('Document status: %s') % STATE_LABELS[document.checkout_state()]
|
||||
]
|
||||
|
||||
if document.is_checked_out():
|
||||
checkout_info = document.checkout_info()
|
||||
paragraphs.append(_('User: %s') % (checkout_info.user.get_full_name() or checkout_info.user))
|
||||
paragraphs.append(_('Check out time: %s') % checkout_info.checkout_datetime)
|
||||
paragraphs.append(_('Check out expiration: %s') % checkout_info.expiration_datetime)
|
||||
paragraphs.append(_('New versions allowed: %s') % (_('Yes') if not checkout_info.block_new_version else _('No')))
|
||||
paragraphs.append(
|
||||
_('User: %s') % (checkout_info.user.get_full_name() or checkout_info.user)
|
||||
)
|
||||
paragraphs.append(
|
||||
_('Check out time: %s') % checkout_info.checkout_datetime
|
||||
)
|
||||
paragraphs.append(
|
||||
_('Check out expiration: %s') % checkout_info.expiration_datetime
|
||||
)
|
||||
paragraphs.append(
|
||||
_('New versions allowed: %s') % (_('Yes') if not checkout_info.block_new_version else _('No'))
|
||||
)
|
||||
|
||||
return render_to_response('appearance/generic_template.html', {
|
||||
'paragraphs': paragraphs,
|
||||
'object': document,
|
||||
'title': _('Check out details for document: %s') % document
|
||||
}, context_instance=RequestContext(request))
|
||||
return render_to_response(
|
||||
'appearance/generic_template.html', {
|
||||
'paragraphs': paragraphs,
|
||||
'object': document,
|
||||
'title': _('Check out details for document: %s') % document
|
||||
},
|
||||
context_instance=RequestContext(request)
|
||||
)
|
||||
|
||||
|
||||
def checkin_document(request, document_pk):
|
||||
document = get_object_or_404(Document, pk=document_pk)
|
||||
post_action_redirect = reverse('checkouts:checkout_info', args=[document.pk])
|
||||
post_action_redirect = reverse(
|
||||
'checkouts:checkout_info', args=[document.pk]
|
||||
)
|
||||
|
||||
previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
|
||||
@@ -16,8 +16,15 @@ class SplitTimeDeltaWidget(forms.widgets.MultiWidget):
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
widgets = (
|
||||
forms.widgets.NumberInput(attrs={'maxlength': 4, 'style': 'width: 8em;', 'placeholder': _('Period')}),
|
||||
forms.widgets.Select(attrs={'style': 'width: 8em;'}, choices=TIME_DELTA_UNIT_CHOICES),
|
||||
forms.widgets.NumberInput(
|
||||
attrs={
|
||||
'maxlength': 4, 'style': 'width: 8em;',
|
||||
'placeholder': _('Period')
|
||||
}
|
||||
),
|
||||
forms.widgets.Select(
|
||||
attrs={'style': 'width: 8em;'}, choices=TIME_DELTA_UNIT_CHOICES
|
||||
),
|
||||
|
||||
)
|
||||
super(SplitTimeDeltaWidget, self).__init__(widgets, attrs)
|
||||
|
||||
@@ -40,7 +40,13 @@ class MayanAppConfig(apps.AppConfig):
|
||||
else:
|
||||
top_url = '{}/'.format(self.name)
|
||||
|
||||
urlpatterns += url(r'^{}'.format(top_url), include('{}.urls'.format(self.name), namespace=self.app_namespace or self.name)),
|
||||
urlpatterns += url(
|
||||
r'^{}'.format(top_url),
|
||||
include(
|
||||
'{}.urls'.format(self.name),
|
||||
namespace=self.app_namespace or self.name
|
||||
)
|
||||
),
|
||||
|
||||
|
||||
class CommonApp(MayanAppConfig):
|
||||
@@ -60,7 +66,19 @@ class CommonApp(MayanAppConfig):
|
||||
|
||||
app.conf.CELERY_DEFAULT_QUEUE = 'default'
|
||||
|
||||
menu_facet.bind_links(links=[link_current_user_details, link_current_user_locale_profile_details, link_tools, link_setup], sources=['common:current_user_details', 'common:current_user_edit', 'common:current_user_locale_profile_details', 'common:current_user_locale_profile_edit', 'authentication:password_change_view', 'common:setup_list', 'common:tools_list'])
|
||||
menu_facet.bind_links(
|
||||
links=[
|
||||
link_current_user_details,
|
||||
link_current_user_locale_profile_details, link_tools,
|
||||
link_setup
|
||||
], sources=[
|
||||
'common:current_user_details', 'common:current_user_edit',
|
||||
'common:current_user_locale_profile_details',
|
||||
'common:current_user_locale_profile_edit',
|
||||
'authentication:password_change_view', 'common:setup_list',
|
||||
'common:tools_list'
|
||||
]
|
||||
)
|
||||
menu_main.bind_links(links=[link_about], position=-1)
|
||||
menu_secondary.bind_links(
|
||||
links=[link_about, link_license],
|
||||
@@ -70,7 +88,20 @@ class CommonApp(MayanAppConfig):
|
||||
links=[
|
||||
link_current_user_edit, link_current_user_locale_profile_edit
|
||||
],
|
||||
sources=['common:current_user_details', 'common:current_user_edit', 'common:current_user_locale_profile_details', 'common:current_user_locale_profile_edit', 'authentication:password_change_view', 'common:setup_list', 'common:tools_list']
|
||||
sources=[
|
||||
'common:current_user_details', 'common:current_user_edit',
|
||||
'common:current_user_locale_profile_details',
|
||||
'common:current_user_locale_profile_edit',
|
||||
'authentication:password_change_view', 'common:setup_list',
|
||||
'common:tools_list'
|
||||
]
|
||||
)
|
||||
user_logged_in.connect(
|
||||
user_locale_profile_session_config,
|
||||
dispatch_uid='user_locale_profile_session_config'
|
||||
)
|
||||
post_save.connect(
|
||||
user_locale_profile_create,
|
||||
dispatch_uid='user_locale_profile_create',
|
||||
sender=settings.AUTH_USER_MODEL
|
||||
)
|
||||
user_logged_in.connect(user_locale_profile_session_config, dispatch_uid='user_locale_profile_session_config')
|
||||
post_save.connect(user_locale_profile_create, dispatch_uid='user_locale_profile_create', sender=settings.AUTH_USER_MODEL)
|
||||
|
||||
@@ -29,7 +29,9 @@ class ModelAttribute(object):
|
||||
|
||||
@classmethod
|
||||
def get_choices_for(cls, model, type_names=None):
|
||||
return [(attribute.name, attribute) for attribute in cls.get_for(model, type_names)]
|
||||
return [
|
||||
(attribute.name, attribute) for attribute in cls.get_for(model, type_names)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def help_text_for(cls, model, type_names=None):
|
||||
@@ -41,7 +43,9 @@ class ModelAttribute(object):
|
||||
|
||||
def get_display(self, show_name=False):
|
||||
if self.description:
|
||||
return '{} - {}'.format(self.name if show_name else self.label, self.description)
|
||||
return '{} - {}'.format(
|
||||
self.name if show_name else self.label, self.description
|
||||
)
|
||||
else:
|
||||
return unicode(self.name if show_name else self.label)
|
||||
|
||||
|
||||
@@ -63,12 +63,16 @@ class CompressedFile(object):
|
||||
file_input.seek(0)
|
||||
except AttributeError:
|
||||
# If not, keep it
|
||||
self.zf.write(file_input, arcname=arcname, compress_type=COMPRESSION)
|
||||
self.zf.write(
|
||||
file_input, arcname=arcname, compress_type=COMPRESSION
|
||||
)
|
||||
else:
|
||||
self.zf.writestr(arcname, file_input.read())
|
||||
|
||||
def contents(self):
|
||||
return [filename for filename in self.zf.namelist() if not filename.endswith('/')]
|
||||
return [
|
||||
filename for filename in self.zf.namelist() if not filename.endswith('/')
|
||||
]
|
||||
|
||||
def get_content(self, filename):
|
||||
return self.zf.read(filename)
|
||||
@@ -93,8 +97,12 @@ class CompressedFile(object):
|
||||
try:
|
||||
# Try for a ZIP file
|
||||
zfobj = zipfile.ZipFile(self.file_object)
|
||||
filenames = [filename for filename in zfobj.namelist() if not filename.endswith('/')]
|
||||
return (SimpleUploadedFile(name=filename, content=zfobj.read(filename)) for filename in filenames)
|
||||
filenames = [
|
||||
filename for filename in zfobj.namelist() if not filename.endswith('/')
|
||||
]
|
||||
return (
|
||||
SimpleUploadedFile(name=filename, content=zfobj.read(filename)) for filename in filenames
|
||||
)
|
||||
except zipfile.BadZipfile:
|
||||
raise NotACompressedFile
|
||||
|
||||
|
||||
@@ -72,7 +72,9 @@ class ChoiceForm(forms.Form):
|
||||
self.fields['selection'].label = label
|
||||
self.fields['selection'].help_text = help_text
|
||||
self.fields['selection'].widget.disabled_choices = disabled_choices
|
||||
self.fields['selection'].widget.attrs.update({'size': 14, 'class': 'choice_form'})
|
||||
self.fields['selection'].widget.attrs.update(
|
||||
{'size': 14, 'class': 'choice_form'}
|
||||
)
|
||||
|
||||
selection = forms.MultipleChoiceField(widget=DisableableSelectWidget())
|
||||
|
||||
@@ -84,7 +86,10 @@ class UserForm_view(DetailForm):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('username', 'first_name', 'last_name', 'email', 'is_staff', 'is_superuser', 'last_login', 'date_joined', 'groups')
|
||||
fields = (
|
||||
'username', 'first_name', 'last_name', 'email', 'is_staff',
|
||||
'is_superuser', 'last_login', 'date_joined', 'groups'
|
||||
)
|
||||
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
@@ -119,7 +124,9 @@ class FileDisplayForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FileDisplayForm, self).__init__(*args, **kwargs)
|
||||
changelog_path = os.path.join(settings.BASE_DIR, os.sep.join(self.DIRECTORY), self.FILENAME)
|
||||
changelog_path = os.path.join(
|
||||
settings.BASE_DIR, os.sep.join(self.DIRECTORY), self.FILENAME
|
||||
)
|
||||
fd = open(changelog_path)
|
||||
self.fields['text'].initial = fd.read()
|
||||
fd.close()
|
||||
|
||||
@@ -79,8 +79,14 @@ class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermis
|
||||
return self.right_list_help_text
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.unselected_list = ChoiceForm(prefix=self.LEFT_LIST_NAME, choices=self.left_list())
|
||||
self.selected_list = ChoiceForm(prefix=self.RIGHT_LIST_NAME, choices=self.right_list(), disabled_choices=self.get_disabled_choices(), help_text=self.get_right_list_help_text())
|
||||
self.unselected_list = ChoiceForm(
|
||||
prefix=self.LEFT_LIST_NAME, choices=self.left_list()
|
||||
)
|
||||
self.selected_list = ChoiceForm(
|
||||
prefix=self.RIGHT_LIST_NAME, choices=self.right_list(),
|
||||
disabled_choices=self.get_disabled_choices(),
|
||||
help_text=self.get_right_list_help_text()
|
||||
)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def process_form(self, prefix, items_function, action_function):
|
||||
@@ -112,11 +118,20 @@ class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermis
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
else:
|
||||
messages.error(self.request, _('Unable to transfer selection: %s.') % label)
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Unable to transfer selection: %s.') % label
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.process_form(prefix=self.LEFT_LIST_NAME, items_function=self.left_list, action_function=self.add)
|
||||
self.process_form(prefix=self.RIGHT_LIST_NAME, items_function=self.right_list, action_function=self.remove)
|
||||
self.process_form(
|
||||
prefix=self.LEFT_LIST_NAME, items_function=self.left_list,
|
||||
action_function=self.add
|
||||
)
|
||||
self.process_form(
|
||||
prefix=self.RIGHT_LIST_NAME, items_function=self.right_list,
|
||||
action_function=self.remove
|
||||
)
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -183,7 +198,11 @@ class MultiFormView(FormView):
|
||||
return form
|
||||
|
||||
def get_forms(self, form_classes):
|
||||
return dict([(key, self._create_form(key, klass)) for key, klass in form_classes.items()])
|
||||
return dict(
|
||||
[
|
||||
(key, self._create_form(key, klass)) for key, klass in form_classes.items()
|
||||
]
|
||||
)
|
||||
|
||||
def get_initial(self, form_name):
|
||||
initial_method = 'get_%s_initial' % form_name
|
||||
@@ -246,8 +265,13 @@ class ParentChildListView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin,
|
||||
else:
|
||||
is_empty = len(self.object_list) == 0
|
||||
if is_empty:
|
||||
raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.")
|
||||
% {'class_name': self.__class__.__name__})
|
||||
raise Http404(
|
||||
_(
|
||||
"Empty list and '%(class_name)s.allow_empty' is False."
|
||||
) % {
|
||||
'class_name': self.__class__.__name__
|
||||
}
|
||||
)
|
||||
|
||||
context = self.get_context_data(object=self.object)
|
||||
return self.render_to_response(context)
|
||||
@@ -313,7 +337,10 @@ class SingleObjectCreateView(ViewPermissionCheckMixin, ExtraContextMixin, Redire
|
||||
result = super(SingleObjectCreateView, self).form_invalid(form)
|
||||
|
||||
try:
|
||||
messages.error(self.request, _('Error creating new %s.') % self.extra_context['object_name'])
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Error creating new %s.') % self.extra_context['object_name']
|
||||
)
|
||||
except KeyError:
|
||||
messages.error(self.request, _('Error creating object.'))
|
||||
|
||||
@@ -322,9 +349,14 @@ class SingleObjectCreateView(ViewPermissionCheckMixin, ExtraContextMixin, Redire
|
||||
def form_valid(self, form):
|
||||
result = super(SingleObjectCreateView, self).form_valid(form)
|
||||
try:
|
||||
messages.success(self.request, _('%s created successfully.') % self.extra_context['object_name'].capitalize())
|
||||
messages.success(
|
||||
self.request,
|
||||
_('%s created successfully.') % self.extra_context['object_name'].capitalize()
|
||||
)
|
||||
except KeyError:
|
||||
messages.success(self.request, _('New object created successfully.'))
|
||||
messages.success(
|
||||
self.request, _('New object created successfully.')
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -342,16 +374,24 @@ class SingleObjectDeleteView(ViewPermissionCheckMixin, ObjectPermissionCheckMixi
|
||||
result = super(SingleObjectDeleteView, self).delete(request, *args, **kwargs)
|
||||
except Exception as exception:
|
||||
try:
|
||||
messages.error(self.request, _('Error deleting %s.') % self.extra_context['object_name'])
|
||||
messages.error(
|
||||
self.request, _('Error deleting %s.') % self.extra_context['object_name']
|
||||
)
|
||||
except KeyError:
|
||||
messages.error(self.request, _('Error deleting object.'))
|
||||
messages.error(
|
||||
self.request, _('Error deleting object.')
|
||||
)
|
||||
|
||||
raise exception
|
||||
else:
|
||||
try:
|
||||
messages.success(self.request, _('%s deleted successfully.') % self.extra_context['object_name'].capitalize())
|
||||
messages.success(
|
||||
self.request, _('%s deleted successfully.') % self.extra_context['object_name'].capitalize()
|
||||
)
|
||||
except KeyError:
|
||||
messages.success(self.request, _('Object deleted successfully.'))
|
||||
messages.success(
|
||||
self.request, _('Object deleted successfully.')
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -365,9 +405,13 @@ class SingleObjectEditView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin,
|
||||
result = super(SingleObjectEditView, self).form_invalid(form)
|
||||
|
||||
try:
|
||||
messages.error(self.request, _('Error saving %s details.') % self.extra_context['object_name'])
|
||||
messages.error(
|
||||
self.request, _('Error saving %s details.') % self.extra_context['object_name']
|
||||
)
|
||||
except KeyError:
|
||||
messages.error(self.request, _('Error saving details.'))
|
||||
messages.error(
|
||||
self.request, _('Error saving details.')
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -375,9 +419,13 @@ class SingleObjectEditView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin,
|
||||
result = super(SingleObjectEditView, self).form_valid(form)
|
||||
|
||||
try:
|
||||
messages.success(self.request, _('%s details saved successfully.') % self.extra_context['object_name'].capitalize())
|
||||
messages.success(
|
||||
self.request, _('%s details saved successfully.') % self.extra_context['object_name'].capitalize()
|
||||
)
|
||||
except KeyError:
|
||||
messages.success(self.request, _('Details saved successfully.'))
|
||||
messages.success(
|
||||
self.request, _('Details saved successfully.')
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ from .models import UserLocaleProfile
|
||||
|
||||
|
||||
def user_locale_profile_session_config(sender, request, user, **kwargs):
|
||||
user_locale_profile, created = UserLocaleProfile.objects.get_or_create(user=user)
|
||||
user_locale_profile, created = UserLocaleProfile.objects.get_or_create(
|
||||
user=user
|
||||
)
|
||||
|
||||
if not created and user_locale_profile.timezone and user_locale_profile.language:
|
||||
# Don't try to set locale preferences for new users or existing users
|
||||
@@ -16,11 +18,19 @@ def user_locale_profile_session_config(sender, request, user, **kwargs):
|
||||
translation.activate(user_locale_profile.language)
|
||||
|
||||
if hasattr(request, 'session'):
|
||||
request.session[translation.LANGUAGE_SESSION_KEY] = user_locale_profile.language
|
||||
request.session[settings.TIMEZONE_SESSION_KEY] = user_locale_profile.timezone
|
||||
request.session[
|
||||
translation.LANGUAGE_SESSION_KEY
|
||||
] = user_locale_profile.language
|
||||
request.session[
|
||||
settings.TIMEZONE_SESSION_KEY
|
||||
] = user_locale_profile.timezone
|
||||
else:
|
||||
request.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_locale_profile.language)
|
||||
request.set_cookie(settings.TIMEZONE_COOKIE_NAME, user_locale_profile.timezone)
|
||||
request.set_cookie(
|
||||
settings.LANGUAGE_COOKIE_NAME, user_locale_profile.language
|
||||
)
|
||||
request.set_cookie(
|
||||
settings.TIMEZONE_COOKIE_NAME, user_locale_profile.timezone
|
||||
)
|
||||
|
||||
|
||||
def user_locale_profile_create(sender, instance, created, **kwargs):
|
||||
|
||||
@@ -5,15 +5,30 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from navigation import Link
|
||||
|
||||
|
||||
def is_superuser(context):
|
||||
return context['request'].user.is_staff or context['request'].user.is_superuser
|
||||
|
||||
|
||||
link_about = Link(icon='fa fa-question', text=_('About'), view='common:about_view')
|
||||
link_current_user_details = Link(icon='fa fa-user', text=_('User details'), view='common:current_user_details')
|
||||
link_current_user_edit = Link(icon='fa fa-user', text=_('Edit details'), view='common:current_user_edit')
|
||||
link_current_user_locale_profile_details = Link(icon='fa fa-globe', text=_('Locale profile'), view='common:current_user_locale_profile_details')
|
||||
link_current_user_locale_profile_edit = Link(icon='fa fa-globe', text=_('Edit locale profile'), view='common:current_user_locale_profile_edit')
|
||||
link_license = Link(icon='fa fa-book', text=_('License'), view='common:license_view')
|
||||
link_setup = Link(icon='fa fa-gear', text=_('Setup'), view='common:setup_list')
|
||||
link_tools = Link(icon='fa fa-wrench', text=_('Tools'), view='common:tools_list')
|
||||
link_about = Link(
|
||||
icon='fa fa-question', text=_('About'), view='common:about_view'
|
||||
)
|
||||
link_current_user_details = Link(
|
||||
icon='fa fa-user', text=_('User details'),
|
||||
view='common:current_user_details'
|
||||
)
|
||||
link_current_user_edit = Link(
|
||||
icon='fa fa-user', text=_('Edit details'), view='common:current_user_edit'
|
||||
)
|
||||
link_current_user_locale_profile_details = Link(
|
||||
icon='fa fa-globe', text=_('Locale profile'),
|
||||
view='common:current_user_locale_profile_details'
|
||||
)
|
||||
link_current_user_locale_profile_edit = Link(
|
||||
icon='fa fa-globe', text=_('Edit locale profile'),
|
||||
view='common:current_user_locale_profile_edit'
|
||||
)
|
||||
link_license = Link(
|
||||
icon='fa fa-book', text=_('License'), view='common:license_view'
|
||||
)
|
||||
link_setup = Link(
|
||||
icon='fa fa-gear', text=_('Setup'), view='common:setup_list'
|
||||
)
|
||||
link_tools = Link(
|
||||
icon='fa fa-wrench', text=_('Tools'), view='common:tools_list'
|
||||
)
|
||||
|
||||
@@ -45,10 +45,14 @@ class ObjectListPermissionFilterMixin(object):
|
||||
if self.object_permission:
|
||||
try:
|
||||
# Check to see if the user has the permissions globally
|
||||
Permission.check_permissions(self.request.user, (self.object_permission,))
|
||||
Permission.check_permissions(
|
||||
self.request.user, (self.object_permission,)
|
||||
)
|
||||
except PermissionDenied:
|
||||
# No global permission, filter ther queryset per object + permission
|
||||
return AccessControlList.objects.filter_by_access(self.object_permission, self.request.user, queryset)
|
||||
return AccessControlList.objects.filter_by_access(
|
||||
self.object_permission, self.request.user, queryset
|
||||
)
|
||||
else:
|
||||
# Has the permission globally, return all results
|
||||
return queryset
|
||||
@@ -66,9 +70,14 @@ class ObjectPermissionCheckMixin(object):
|
||||
|
||||
if self.object_permission:
|
||||
try:
|
||||
Permission.check_permissions(request.user, (self.object_permission,))
|
||||
Permission.check_permissions(
|
||||
request.user, (self.object_permission,)
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(self.object_permission, request.user, self.get_permission_object())
|
||||
AccessControlList.objects.check_access(
|
||||
self.object_permission, request.user,
|
||||
self.get_permission_object()
|
||||
)
|
||||
|
||||
return super(ObjectPermissionCheckMixin, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -87,8 +96,20 @@ class RedirectionMixin(object):
|
||||
post_action_redirect = self.get_post_action_redirect()
|
||||
action_cancel_redirect = self.get_action_cancel_redirect()
|
||||
|
||||
self.next_url = self.request.POST.get('next', self.request.GET.get('next', post_action_redirect if post_action_redirect else self.request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
self.previous_url = self.request.POST.get('previous', self.request.GET.get('previous', action_cancel_redirect if action_cancel_redirect else self.request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
self.next_url = self.request.POST.get(
|
||||
'next', self.request.GET.get(
|
||||
'next', post_action_redirect if post_action_redirect else self.request.META.get(
|
||||
'HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)
|
||||
)
|
||||
)
|
||||
)
|
||||
self.previous_url = self.request.POST.get(
|
||||
'previous', self.request.GET.get(
|
||||
'previous', action_cancel_redirect if action_cancel_redirect else self.request.META.get(
|
||||
'HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return super(RedirectionMixin, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -112,6 +133,8 @@ class ViewPermissionCheckMixin(object):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if self.view_permission:
|
||||
Permission.check_permissions(self.request.user, (self.view_permission,))
|
||||
Permission.check_permissions(
|
||||
self.request.user, (self.view_permission,)
|
||||
)
|
||||
|
||||
return super(ViewPermissionCheckMixin, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -18,9 +18,14 @@ def upload_to(instance, filename):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class SharedUploadedFile(models.Model):
|
||||
file = models.FileField(storage=shared_storage_backend, upload_to=upload_to, verbose_name=_('File'))
|
||||
file = models.FileField(
|
||||
storage=shared_storage_backend, upload_to=upload_to,
|
||||
verbose_name=_('File')
|
||||
)
|
||||
filename = models.CharField(max_length=255, verbose_name=_('Filename'))
|
||||
datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Date time'))
|
||||
datetime = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name=_('Date time')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Shared uploaded file')
|
||||
@@ -43,10 +48,17 @@ class SharedUploadedFile(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class UserLocaleProfile(models.Model):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='locale_profile', verbose_name=_('User'))
|
||||
|
||||
timezone = models.CharField(choices=zip(common_timezones, common_timezones), max_length=48, verbose_name=_('Timezone'))
|
||||
language = models.CharField(choices=settings.LANGUAGES, max_length=8, verbose_name=_('Language'))
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL, related_name='locale_profile',
|
||||
verbose_name=_('User')
|
||||
)
|
||||
timezone = models.CharField(
|
||||
choices=zip(common_timezones, common_timezones), max_length=48,
|
||||
verbose_name=_('Timezone')
|
||||
)
|
||||
language = models.CharField(
|
||||
choices=settings.LANGUAGES, max_length=8, verbose_name=_('Language')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self.user)
|
||||
|
||||
@@ -5,5 +5,13 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from smart_settings import Namespace
|
||||
|
||||
namespace = Namespace(name='common', label=_('Common'))
|
||||
setting_temporary_directory = namespace.add_setting(global_name='COMMON_TEMPORARY_DIRECTORY', default='/tmp', help_text=_('Temporary directory used site wide to store thumbnails, previews and temporary files. If none is specified, one will be created using tempfile.mkdtemp()'), is_path=True) # TODO: get os default temp directory
|
||||
setting_shared_storage = namespace.add_setting(global_name='COMMON_SHARED_STORAGE', default='storage.backends.filebasedstorage.FileBasedStorage', help_text=_('A storage backend that all workers can use to share files.'))
|
||||
setting_temporary_directory = namespace.add_setting(
|
||||
global_name='COMMON_TEMPORARY_DIRECTORY', default='/tmp',
|
||||
help_text=_('Temporary directory used site wide to store thumbnails, previews and temporary files. If none is specified, one will be created using tempfile.mkdtemp()'),
|
||||
is_path=True
|
||||
) # TODO: get os default temp directory
|
||||
setting_shared_storage = namespace.add_setting(
|
||||
global_name='COMMON_SHARED_STORAGE',
|
||||
default='storage.backends.filebasedstorage.FileBasedStorage',
|
||||
help_text=_('A storage backend that all workers can use to share files.')
|
||||
)
|
||||
|
||||
@@ -15,17 +15,38 @@ urlpatterns = patterns(
|
||||
url(r'^$', HomeView.as_view(), name='home'),
|
||||
url(r'^about/$', AboutView.as_view(), name='about_view'),
|
||||
url(r'^license/$', LicenseView.as_view(), name='license_view'),
|
||||
url(r'^object/multiple/action/$', 'multi_object_action_view', name='multi_object_action_view'),
|
||||
url(
|
||||
r'^object/multiple/action/$', 'multi_object_action_view',
|
||||
name='multi_object_action_view'
|
||||
),
|
||||
url(r'^setup/$', SetupListView.as_view(), name='setup_list'),
|
||||
url(r'^tools/$', ToolsListView.as_view(), name='tools_list'),
|
||||
url(r'^user/$', CurrentUserDetailsView.as_view(), name='current_user_details'),
|
||||
url(r'^user/edit/$', CurrentUserEditView.as_view(), name='current_user_edit'),
|
||||
url(r'^user/locale/$', CurrentUserLocaleProfileDetailsView.as_view(), name='current_user_locale_profile_details'),
|
||||
url(r'^user/locale/edit/$', CurrentUserLocaleProfileEditView.as_view(), name='current_user_locale_profile_edit'),
|
||||
url(
|
||||
r'^user/$', CurrentUserDetailsView.as_view(),
|
||||
name='current_user_details'
|
||||
),
|
||||
url(
|
||||
r'^user/edit/$', CurrentUserEditView.as_view(),
|
||||
name='current_user_edit'
|
||||
),
|
||||
url(
|
||||
r'^user/locale/$', CurrentUserLocaleProfileDetailsView.as_view(),
|
||||
name='current_user_locale_profile_details'
|
||||
),
|
||||
url(
|
||||
r'^user/locale/edit/$', CurrentUserLocaleProfileEditView.as_view(),
|
||||
name='current_user_locale_profile_edit'
|
||||
),
|
||||
)
|
||||
|
||||
urlpatterns += patterns(
|
||||
'',
|
||||
url(r'^set_language/$', 'django.views.i18n.set_language', name='set_language'),
|
||||
(r'^favicon\.ico$', RedirectView.as_view(url=static('appearance/images/favicon.ico'))),
|
||||
url(
|
||||
r'^set_language/$', 'django.views.i18n.set_language',
|
||||
name='set_language'
|
||||
),
|
||||
(
|
||||
r'^favicon\.ico$',
|
||||
RedirectView.as_view(url=static('appearance/images/favicon.ico'))
|
||||
),
|
||||
)
|
||||
|
||||
@@ -37,7 +37,8 @@ def copyfile(source, destination, buffer_size=1024 * 1024):
|
||||
def encapsulate(function):
|
||||
# Workaround Django ticket 15791
|
||||
# Changeset 16045
|
||||
# http://stackoverflow.com/questions/6861601/cannot-resolve-callable-context-variable/6955045#6955045
|
||||
# http://stackoverflow.com/questions/6861601/
|
||||
# cannot-resolve-callable-context-variable/6955045#6955045
|
||||
return lambda: function
|
||||
|
||||
|
||||
|
||||
@@ -53,9 +53,13 @@ class CurrentUserLocaleProfileDetailsView(TemplateView):
|
||||
template_name = 'appearance/generic_form.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super(CurrentUserLocaleProfileDetailsView, self).get_context_data(**kwargs)
|
||||
data = super(
|
||||
CurrentUserLocaleProfileDetailsView, self
|
||||
).get_context_data(**kwargs)
|
||||
data.update({
|
||||
'form': LocaleProfileForm_view(instance=self.request.user.locale_profile),
|
||||
'form': LocaleProfileForm_view(
|
||||
instance=self.request.user.locale_profile
|
||||
),
|
||||
'read_only': True,
|
||||
'title': _('Current user locale profile details'),
|
||||
})
|
||||
@@ -63,9 +67,13 @@ class CurrentUserLocaleProfileDetailsView(TemplateView):
|
||||
|
||||
|
||||
class CurrentUserLocaleProfileEditView(SingleObjectEditView):
|
||||
extra_context = {'title': _('Edit current user locale profile details')}
|
||||
extra_context = {
|
||||
'title': _('Edit current user locale profile details')
|
||||
}
|
||||
form_class = LocaleProfileForm
|
||||
post_action_redirect = reverse_lazy('common:current_user_locale_profile_details')
|
||||
post_action_redirect = reverse_lazy(
|
||||
'common:current_user_locale_profile_details'
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
@@ -74,11 +82,19 @@ class CurrentUserLocaleProfileEditView(SingleObjectEditView):
|
||||
translation.activate(form.cleaned_data['language'])
|
||||
|
||||
if hasattr(self.request, 'session'):
|
||||
self.request.session[translation.LANGUAGE_SESSION_KEY] = form.cleaned_data['language']
|
||||
self.request.session[settings.TIMEZONE_SESSION_KEY] = form.cleaned_data['timezone']
|
||||
self.request.session[
|
||||
translation.LANGUAGE_SESSION_KEY
|
||||
] = form.cleaned_data['language']
|
||||
self.request.session[
|
||||
settings.TIMEZONE_SESSION_KEY
|
||||
] = form.cleaned_data['timezone']
|
||||
else:
|
||||
self.request.set_cookie(settings.LANGUAGE_COOKIE_NAME, form.cleaned_data['language'])
|
||||
self.request.set_cookie(settings.TIMEZONE_COOKIE_NAME, form.cleaned_data['timezone'])
|
||||
self.request.set_cookie(
|
||||
settings.LANGUAGE_COOKIE_NAME, form.cleaned_data['language']
|
||||
)
|
||||
self.request.set_cookie(
|
||||
settings.TIMEZONE_COOKIE_NAME, form.cleaned_data['timezone']
|
||||
)
|
||||
|
||||
return super(CurrentUserLocaleProfileEditView, self).form_valid(form)
|
||||
|
||||
@@ -95,14 +111,18 @@ class HomeView(TemplateView):
|
||||
'hide_links': True,
|
||||
'search_results_limit': 100,
|
||||
'search_terms': self.request.GET.get('q'),
|
||||
'missing_list': [item for item in MissingItem.get_all() if item.condition()],
|
||||
'missing_list': [
|
||||
item for item in MissingItem.get_all() if item.condition()
|
||||
],
|
||||
})
|
||||
return data
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = self.get_context_data(**kwargs)
|
||||
|
||||
queryset, ids, timedelta = document_search.search(request.GET, request.user)
|
||||
queryset, ids, timedelta = document_search.search(
|
||||
request.GET, request.user
|
||||
)
|
||||
|
||||
# Update the context with the search results
|
||||
context.update({
|
||||
@@ -129,7 +149,9 @@ class SetupListView(TemplateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super(SetupListView, self).get_context_data(**kwargs)
|
||||
data.update({
|
||||
'resolved_links': menu_setup.resolve(context=RequestContext(self.request)),
|
||||
'resolved_links': menu_setup.resolve(
|
||||
context=RequestContext(self.request)
|
||||
),
|
||||
'title': _('Setup items'),
|
||||
})
|
||||
return data
|
||||
@@ -154,26 +176,49 @@ def multi_object_action_view(request):
|
||||
then redirects to the appropiate specialized view
|
||||
"""
|
||||
|
||||
next = request.POST.get('next', request.GET.get('next', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
next = request.POST.get(
|
||||
'next', request.GET.get(
|
||||
'next', request.META.get(
|
||||
'HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
action = request.GET.get('action', None)
|
||||
id_list = ','.join([key[3:] for key in request.GET.keys() if key.startswith('pk_')])
|
||||
items_property_list = [loads(key[11:]) for key in request.GET.keys() if key.startswith('properties_')]
|
||||
items_property_list = [
|
||||
(key[11:]) for key in request.GET.keys() if key.startswith('properties_')
|
||||
]
|
||||
|
||||
if not action:
|
||||
messages.error(request, _('No action selected.'))
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))
|
||||
return HttpResponseRedirect(
|
||||
request.META.get(
|
||||
'HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)
|
||||
)
|
||||
)
|
||||
|
||||
if not id_list and not items_property_list:
|
||||
messages.error(request, _('Must select at least one item.'))
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))
|
||||
return HttpResponseRedirect(
|
||||
request.META.get(
|
||||
'HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)
|
||||
)
|
||||
)
|
||||
|
||||
# Separate redirects to keep backwards compatibility with older
|
||||
# functions that don't expect a properties_list parameter
|
||||
if items_property_list:
|
||||
return HttpResponseRedirect('%s?%s' % (
|
||||
action,
|
||||
urlencode({'items_property_list': dumps(items_property_list), 'next': next}))
|
||||
return HttpResponseRedirect(
|
||||
'%s?%s' % (
|
||||
action,
|
||||
urlencode(
|
||||
{
|
||||
'items_property_list': dumps(items_property_list),
|
||||
'next': next
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
return HttpResponseRedirect('%s?%s' % (
|
||||
|
||||
@@ -118,7 +118,10 @@ class ScrollableCheckboxSelectMultiple(forms.widgets.CheckboxSelectMultiple):
|
||||
value = []
|
||||
has_id = attrs and 'id' in attrs
|
||||
final_attrs = self.build_attrs(attrs, name=name)
|
||||
output = ['<ul class="undecorated_list" style="margin-left: 5px; margin-top: 3px; margin-bottom: 3px;">']
|
||||
# TODO: Move this styling to a CSS class
|
||||
output = [
|
||||
'<ul class="undecorated_list" style="margin-left: 5px; margin-top: 3px; margin-bottom: 3px;">'
|
||||
]
|
||||
# Normalize to strings
|
||||
str_values = set([force_unicode(v) for v in value])
|
||||
for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
|
||||
@@ -130,14 +133,22 @@ class ScrollableCheckboxSelectMultiple(forms.widgets.CheckboxSelectMultiple):
|
||||
else:
|
||||
label_for = ''
|
||||
|
||||
cb = forms.widgets.CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
|
||||
cb = forms.widgets.CheckboxInput(
|
||||
final_attrs, check_test=lambda value: value in str_values
|
||||
)
|
||||
option_value = force_unicode(option_value)
|
||||
rendered_cb = cb.render(name, option_value)
|
||||
option_label = conditional_escape(force_unicode(option_label))
|
||||
output.append('<li><label%s>%s %s</label></li>' % (label_for, rendered_cb, option_label))
|
||||
output.append(
|
||||
'<li><label%s>%s %s</label></li>' % (
|
||||
label_for, rendered_cb, option_label
|
||||
)
|
||||
)
|
||||
output.append('</ul>')
|
||||
|
||||
return mark_safe('<div class="text_area_div">%s</div>' % '\n'.join(output))
|
||||
return mark_safe(
|
||||
'<div class="text_area_div">%s</div>' % '\n'.join(output)
|
||||
)
|
||||
|
||||
|
||||
class TextAreaDiv(forms.widgets.Widget):
|
||||
|
||||
@@ -83,7 +83,9 @@ class ConverterBase(object):
|
||||
|
||||
if not os.path.exists(setting_libreoffice_path.value):
|
||||
raise OfficeConversionError(
|
||||
_('LibreOffice not installed or not found at path: %s') % setting_libreoffice_path.value
|
||||
_(
|
||||
'LibreOffice not installed or not found at path: %s'
|
||||
) % setting_libreoffice_path.value
|
||||
)
|
||||
|
||||
new_file_object, input_filepath = tempfile.mkstemp()
|
||||
|
||||
@@ -1 +1 @@
|
||||
RETRY_DELAY = 20 # TODO: convert this into a config option
|
||||
RETRY_DELAY = 5 # TODO: convert this into a config option
|
||||
|
||||
@@ -97,11 +97,10 @@ class WorkflowInstanceDetailView(SingleObjectListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'hide_object': True,
|
||||
'navigation_object_list': ['object', 'workflow_instance'],
|
||||
'object': self.get_workflow_instance().document,
|
||||
'object_list': self.get_queryset(),
|
||||
'title': _('Log entries'),
|
||||
'hide_object': True,
|
||||
'title': _('Detail of workflow: %(workflow)s') % {
|
||||
'workflow': self.get_workflow_instance()
|
||||
},
|
||||
|
||||
@@ -3,8 +3,8 @@ from __future__ import unicode_literals
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (
|
||||
DeletedDocument, Document, DocumentPage, DocumentType, DocumentTypeFilename,
|
||||
DocumentVersion, RecentDocument
|
||||
DeletedDocument, Document, DocumentPage, DocumentType,
|
||||
DocumentTypeFilename, DocumentVersion, RecentDocument
|
||||
)
|
||||
|
||||
|
||||
@@ -50,7 +50,10 @@ class DocumentTypeAdmin(admin.ModelAdmin):
|
||||
inlines = (
|
||||
DocumentTypeFilenameInline,
|
||||
)
|
||||
list_display = ('label', 'trash_time_period', 'trash_time_unit', 'delete_time_period', 'delete_time_unit')
|
||||
list_display = (
|
||||
'label', 'trash_time_period', 'trash_time_unit', 'delete_time_period',
|
||||
'delete_time_unit'
|
||||
)
|
||||
|
||||
|
||||
class RecentDocumentAdmin(admin.ModelAdmin):
|
||||
|
||||
@@ -67,12 +67,18 @@ class APIDocumentListView(generics.ListCreateAPIView):
|
||||
return super(APIDocumentListView, self).post(*args, **kwargs)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
|
||||
serializer = self.get_serializer(
|
||||
data=request.DATA, files=request.FILES
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
document_type = get_object_or_404(DocumentType, pk=serializer.data['document_type'])
|
||||
document_type = get_object_or_404(
|
||||
DocumentType, pk=serializer.data['document_type']
|
||||
)
|
||||
|
||||
logger.info('Creating document version for document type: %s', document_type)
|
||||
logger.info(
|
||||
'Creating document version for document type: %s', document_type
|
||||
)
|
||||
|
||||
document = Document.objects.create(
|
||||
description=serializer.data['description'] or '',
|
||||
@@ -91,13 +97,17 @@ class APIDocumentListView(generics.ListCreateAPIView):
|
||||
document_id=document.pk, user_id=request.user.pk,
|
||||
)
|
||||
|
||||
logger.info('New document version queued for document: %s', document)
|
||||
logger.info(
|
||||
'New document version queued for document: %s', document
|
||||
)
|
||||
|
||||
serializer.object = document
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED,
|
||||
headers=headers)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -147,7 +157,9 @@ class APIDocumentVersionCreateView(generics.CreateAPIView):
|
||||
mayan_view_permissions = {'POST': [permission_document_new_version]}
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
|
||||
serializer = self.get_serializer(
|
||||
data=request.DATA, files=request.FILES
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
# Nested resource we take the document pk from the URL and insert it
|
||||
@@ -155,7 +167,10 @@ class APIDocumentVersionCreateView(generics.CreateAPIView):
|
||||
# a read only field in the serializer
|
||||
document = get_object_or_404(Document, pk=kwargs['pk'])
|
||||
|
||||
document.new_version(file_object=serializer.object.file, comment=serializer.object.comment, _user=request.user)
|
||||
document.new_version(
|
||||
file_object=serializer.object.file,
|
||||
comment=serializer.object.comment, _user=request.user
|
||||
)
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(status=status.HTTP_202_ACCEPTED, headers=headers)
|
||||
@@ -191,9 +206,13 @@ class APIDocumentImageView(generics.GenericAPIView):
|
||||
document = get_object_or_404(Document, pk=pk)
|
||||
|
||||
try:
|
||||
Permission.check_permissions(request.user, [permission_document_view])
|
||||
Permission.check_permissions(
|
||||
request.user, [permission_document_view]
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(permission_document_view, request.user, document)
|
||||
AccessControlList.objects.check_access(
|
||||
permission_document_view, request.user, document
|
||||
)
|
||||
|
||||
size = request.GET.get('size', setting_display_size.value)
|
||||
|
||||
@@ -214,7 +233,12 @@ class APIDocumentImageView(generics.GenericAPIView):
|
||||
document_page = document.pages.get(page_number=page)
|
||||
|
||||
try:
|
||||
task = task_get_document_page_image.apply_async(kwargs=dict(document_page_id=document_page.pk, size=size, zoom=zoom, rotation=rotation, as_base64=True, version=version))
|
||||
task = task_get_document_page_image.apply_async(
|
||||
kwargs=dict(
|
||||
document_page_id=document_page.pk, size=size, zoom=zoom,
|
||||
rotation=rotation, as_base64=True, version=version
|
||||
)
|
||||
)
|
||||
# TODO: prepend 'data:%s;base64,%s' based on format specified in
|
||||
# async call
|
||||
return Response({
|
||||
@@ -222,9 +246,19 @@ class APIDocumentImageView(generics.GenericAPIView):
|
||||
'data': task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT)
|
||||
})
|
||||
except UnknownFileFormat as exception:
|
||||
return Response({'status': 'error', 'detail': 'unknown_file_format', 'message': unicode(exception)})
|
||||
return Response(
|
||||
{
|
||||
'status': 'error', 'detail': 'unknown_file_format',
|
||||
'message': unicode(exception)
|
||||
}
|
||||
)
|
||||
except UnkownConvertError as exception:
|
||||
return Response({'status': 'error', 'detail': 'converter_error', 'message': unicode(exception)})
|
||||
return Response(
|
||||
{
|
||||
'status': 'error', 'detail': 'converter_error',
|
||||
'message': unicode(exception)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class APIDocumentPageView(generics.RetrieveUpdateAPIView):
|
||||
@@ -322,9 +356,13 @@ class APIDocumentTypeDocumentListView(generics.ListAPIView):
|
||||
def get_queryset(self):
|
||||
document_type = get_object_or_404(DocumentType, pk=self.kwargs['pk'])
|
||||
try:
|
||||
Permission.check_permissions(self.request.user, [permission_document_type_view])
|
||||
Permission.check_permissions(
|
||||
self.request.user, [permission_document_type_view]
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(permission_document_type_view, self.request.user, document_type)
|
||||
AccessControlList.objects.check_access(
|
||||
permission_document_type_view, self.request.user, document_type
|
||||
)
|
||||
|
||||
return document_type.documents.all()
|
||||
|
||||
|
||||
@@ -58,8 +58,8 @@ from .links import (
|
||||
)
|
||||
from .literals import CHECK_DELETE_PERIOD_INTERVAL, CHECK_TRASH_PERIOD_INTERVAL
|
||||
from .models import (
|
||||
DeletedDocument, Document, DocumentPage, DocumentType, DocumentTypeFilename,
|
||||
DocumentVersion
|
||||
DeletedDocument, Document, DocumentPage, DocumentType,
|
||||
DocumentTypeFilename, DocumentVersion
|
||||
)
|
||||
from .permissions import (
|
||||
permission_document_delete, permission_document_download,
|
||||
@@ -82,9 +82,16 @@ class DocumentsApp(MayanAppConfig):
|
||||
|
||||
APIEndPoint('documents')
|
||||
|
||||
MissingItem(label=_('Create a document type'), description=_('Every uploaded document must be assigned a document type, it is the basic way Mayan EDMS categorizes documents.'), condition=lambda: not DocumentType.objects.exists(), view='documents:document_type_list')
|
||||
MissingItem(
|
||||
label=_('Create a document type'),
|
||||
description=_('Every uploaded document must be assigned a document type, it is the basic way Mayan EDMS categorizes documents.'),
|
||||
condition=lambda: not DocumentType.objects.exists(),
|
||||
view='documents:document_type_list'
|
||||
)
|
||||
|
||||
ModelAttribute(Document, label=_('Label'), name='label', type_name='field')
|
||||
ModelAttribute(
|
||||
Document, label=_('Label'), name='label', type_name='field'
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=Document, permissions=(
|
||||
@@ -108,16 +115,34 @@ class DocumentsApp(MayanAppConfig):
|
||||
model=Document, related='document_type',
|
||||
)
|
||||
|
||||
SourceColumn(source=Document, label=_('Thumbnail'), attribute=encapsulate(lambda document: document_thumbnail(document, gallery_name='documents:document_list', title=getattr(document, 'label', None), size=setting_thumbnail_size.value)))
|
||||
SourceColumn(source=Document, label=_('Type'), attribute='document_type')
|
||||
SourceColumn(source=DeletedDocument, label=_('Type'), attribute='document_type')
|
||||
SourceColumn(source=DeletedDocument, label=_('Date time trashed'), attribute='deleted_date_time')
|
||||
SourceColumn(
|
||||
source=Document, label=_('Thumbnail'),
|
||||
attribute=encapsulate(
|
||||
lambda document: document_thumbnail(
|
||||
document, gallery_name='documents:document_list',
|
||||
size=setting_thumbnail_size.value,
|
||||
title=getattr(document, 'label', None),
|
||||
)
|
||||
)
|
||||
)
|
||||
SourceColumn(
|
||||
source=Document, label=_('Type'), attribute='document_type'
|
||||
)
|
||||
SourceColumn(
|
||||
source=DeletedDocument, label=_('Type'), attribute='document_type'
|
||||
)
|
||||
SourceColumn(
|
||||
source=DeletedDocument, label=_('Date time trashed'),
|
||||
attribute='deleted_date_time'
|
||||
)
|
||||
|
||||
app.conf.CELERYBEAT_SCHEDULE.update(
|
||||
{
|
||||
'task_check_delete_periods': {
|
||||
'task': 'documents.tasks.task_check_delete_periods',
|
||||
'schedule': timedelta(seconds=CHECK_DELETE_PERIOD_INTERVAL),
|
||||
'schedule': timedelta(
|
||||
seconds=CHECK_DELETE_PERIOD_INTERVAL
|
||||
),
|
||||
},
|
||||
'task_check_trash_periods': {
|
||||
'task': 'documents.tasks.task_check_trash_periods',
|
||||
@@ -128,8 +153,14 @@ class DocumentsApp(MayanAppConfig):
|
||||
|
||||
app.conf.CELERY_QUEUES.extend(
|
||||
(
|
||||
Queue('converter', Exchange('converter'), routing_key='converter', delivery_mode=1),
|
||||
Queue('documents_periodic', Exchange('documents_periodic'), routing_key='documents_periodic', delivery_mode=1),
|
||||
Queue(
|
||||
'converter', Exchange('converter'),
|
||||
routing_key='converter', delivery_mode=1
|
||||
),
|
||||
Queue(
|
||||
'documents_periodic', Exchange('documents_periodic'),
|
||||
routing_key='documents_periodic', delivery_mode=1
|
||||
),
|
||||
Queue('uploads', Exchange('uploads'), routing_key='uploads'),
|
||||
)
|
||||
)
|
||||
@@ -157,44 +188,135 @@ class DocumentsApp(MayanAppConfig):
|
||||
}
|
||||
)
|
||||
|
||||
menu_front_page.bind_links(links=[link_document_list_recent, link_document_list, link_document_list_deleted])
|
||||
menu_front_page.bind_links(
|
||||
links=[
|
||||
link_document_list_recent, link_document_list,
|
||||
link_document_list_deleted
|
||||
]
|
||||
)
|
||||
menu_setup.bind_links(links=[link_document_type_setup])
|
||||
menu_tools.bind_links(links=[link_clear_image_cache])
|
||||
|
||||
# Document type links
|
||||
menu_object.bind_links(links=[link_document_type_edit, link_document_type_filename_list, link_acl_list, link_document_type_delete], sources=[DocumentType])
|
||||
menu_object.bind_links(links=[link_document_type_filename_edit, link_document_type_filename_delete], sources=[DocumentTypeFilename])
|
||||
menu_secondary.bind_links(links=[link_document_type_list, link_document_type_create], sources=[DocumentType, 'documents:document_type_create', 'documents:document_type_list'])
|
||||
menu_sidebar.bind_links(links=[link_document_type_filename_create], sources=[DocumentTypeFilename, 'documents:document_type_filename_list', 'documents:document_type_filename_create'])
|
||||
|
||||
menu_sidebar.bind_links(links=[link_trash_can_empty], sources=['documents:document_list_deleted', 'documents:trash_can_empty'])
|
||||
menu_object.bind_links(
|
||||
links=[
|
||||
link_document_type_edit, link_document_type_filename_list,
|
||||
link_acl_list, link_document_type_delete
|
||||
], sources=[DocumentType]
|
||||
)
|
||||
menu_object.bind_links(
|
||||
links=[
|
||||
link_document_type_filename_edit,
|
||||
link_document_type_filename_delete
|
||||
], sources=[DocumentTypeFilename]
|
||||
)
|
||||
menu_secondary.bind_links(
|
||||
links=[link_document_type_list, link_document_type_create],
|
||||
sources=[
|
||||
DocumentType, 'documents:document_type_create',
|
||||
'documents:document_type_list'
|
||||
]
|
||||
)
|
||||
menu_sidebar.bind_links(
|
||||
links=[link_document_type_filename_create],
|
||||
sources=[
|
||||
DocumentTypeFilename, 'documents:document_type_filename_list',
|
||||
'documents:document_type_filename_create'
|
||||
]
|
||||
)
|
||||
menu_sidebar.bind_links(
|
||||
links=[link_trash_can_empty],
|
||||
sources=[
|
||||
'documents:document_list_deleted', 'documents:trash_can_empty'
|
||||
]
|
||||
)
|
||||
|
||||
# Document object links
|
||||
menu_object.bind_links(links=[link_document_edit, link_document_document_type_edit, link_document_print, link_document_trash, link_document_download, link_document_clear_transformations, link_document_update_page_count], sources=[Document])
|
||||
menu_object.bind_links(links=[link_document_restore, link_document_delete], sources=[DeletedDocument])
|
||||
menu_object.bind_links(
|
||||
links=[
|
||||
link_document_edit, link_document_document_type_edit,
|
||||
link_document_print, link_document_trash,
|
||||
link_document_download, link_document_clear_transformations,
|
||||
link_document_update_page_count
|
||||
], sources=[Document]
|
||||
)
|
||||
menu_object.bind_links(
|
||||
links=[link_document_restore, link_document_delete],
|
||||
sources=[DeletedDocument]
|
||||
)
|
||||
|
||||
# Document facet links
|
||||
menu_facet.bind_links(links=[link_acl_list], sources=[Document])
|
||||
menu_facet.bind_links(links=[link_document_preview], sources=[Document], position=0)
|
||||
menu_facet.bind_links(links=[link_document_properties], sources=[Document], position=2)
|
||||
menu_facet.bind_links(links=[link_events_for_object, link_document_version_list], sources=[Document], position=2)
|
||||
menu_facet.bind_links(
|
||||
links=[link_document_preview], sources=[Document], position=0
|
||||
)
|
||||
menu_facet.bind_links(
|
||||
links=[link_document_properties], sources=[Document], position=2
|
||||
)
|
||||
menu_facet.bind_links(
|
||||
links=[link_events_for_object, link_document_version_list],
|
||||
sources=[Document], position=2
|
||||
)
|
||||
menu_facet.bind_links(links=[link_document_pages], sources=[Document])
|
||||
|
||||
# Document actions
|
||||
menu_object.bind_links(links=[link_document_version_revert, link_document_version_download], sources=[DocumentVersion])
|
||||
menu_multi_item.bind_links(links=[link_document_multiple_clear_transformations, link_document_multiple_trash, link_document_multiple_download, link_document_multiple_update_page_count, link_document_multiple_document_type_edit], sources=[Document])
|
||||
menu_multi_item.bind_links(links=[link_document_multiple_restore, link_document_multiple_delete], sources=[DeletedDocument])
|
||||
menu_object.bind_links(
|
||||
links=[
|
||||
link_document_version_revert, link_document_version_download
|
||||
],
|
||||
sources=[DocumentVersion]
|
||||
)
|
||||
menu_multi_item.bind_links(
|
||||
links=[
|
||||
link_document_multiple_clear_transformations,
|
||||
link_document_multiple_trash, link_document_multiple_download,
|
||||
link_document_multiple_update_page_count,
|
||||
link_document_multiple_document_type_edit
|
||||
], sources=[Document]
|
||||
)
|
||||
menu_multi_item.bind_links(
|
||||
links=[
|
||||
link_document_multiple_restore, link_document_multiple_delete
|
||||
], sources=[DeletedDocument]
|
||||
)
|
||||
|
||||
# Document pages
|
||||
menu_facet.bind_links(links=[link_document_page_rotate_left, link_document_page_rotate_right, link_document_page_zoom_in, link_document_page_zoom_out, link_document_page_view_reset], sources=['documents:document_page_view'])
|
||||
menu_facet.bind_links(links=[link_document_page_return, link_document_page_view], sources=[DocumentPage])
|
||||
menu_facet.bind_links(links=[link_document_page_navigation_first, link_document_page_navigation_previous, link_document_page_navigation_next, link_document_page_navigation_last, link_transformation_list], sources=[DocumentPage])
|
||||
menu_object.bind_links(links=[link_transformation_list], sources=[DocumentPage])
|
||||
menu_facet.bind_links(
|
||||
links=[
|
||||
link_document_page_rotate_left,
|
||||
link_document_page_rotate_right, link_document_page_zoom_in,
|
||||
link_document_page_zoom_out, link_document_page_view_reset
|
||||
], sources=['documents:document_page_view']
|
||||
)
|
||||
menu_facet.bind_links(
|
||||
links=[link_document_page_return, link_document_page_view],
|
||||
sources=[DocumentPage]
|
||||
)
|
||||
menu_facet.bind_links(
|
||||
links=[
|
||||
link_document_page_navigation_first,
|
||||
link_document_page_navigation_previous,
|
||||
link_document_page_navigation_next,
|
||||
link_document_page_navigation_last, link_transformation_list
|
||||
], sources=[DocumentPage]
|
||||
)
|
||||
menu_object.bind_links(
|
||||
links=[link_transformation_list], sources=[DocumentPage]
|
||||
)
|
||||
|
||||
namespace = StatisticNamespace(name='documents', label=_('Documents'))
|
||||
namespace.add_statistic(DocumentStatistics(name='document_stats', label=_('Document tendencies')))
|
||||
namespace.add_statistic(DocumentUsageStatistics(name='document_usage', label=_('Document usage')))
|
||||
namespace.add_statistic(
|
||||
DocumentStatistics(
|
||||
name='document_stats', label=_('Document tendencies')
|
||||
)
|
||||
)
|
||||
namespace.add_statistic(DocumentUsageStatistics(
|
||||
name='document_usage', label=_('Document usage'))
|
||||
)
|
||||
|
||||
post_initial_setup.connect(create_default_document_type, dispatch_uid='create_default_document_type')
|
||||
post_initial_setup.connect(
|
||||
create_default_document_type,
|
||||
dispatch_uid='create_default_document_type'
|
||||
)
|
||||
|
||||
registry.register(Document)
|
||||
|
||||
@@ -4,8 +4,19 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from events.classes import Event
|
||||
|
||||
event_document_create = Event(name='documents_document_create', label=_('Document created'))
|
||||
event_document_properties_edit = Event(name='documents_document_edit', label=_('Document properties edited'))
|
||||
event_document_type_change = Event(name='documents_document_type_change', label=_('Document type changed'))
|
||||
event_document_new_version = Event(name='documents_document_new_version', label=_('New version uploaded'))
|
||||
event_document_version_revert = Event(name='documents_document_version_revert', label=_('Document version reverted'))
|
||||
event_document_create = Event(
|
||||
name='documents_document_create', label=_('Document created')
|
||||
)
|
||||
event_document_properties_edit = Event(
|
||||
name='documents_document_edit', label=_('Document properties edited')
|
||||
)
|
||||
event_document_type_change = Event(
|
||||
name='documents_document_type_change', label=_('Document type changed')
|
||||
)
|
||||
event_document_new_version = Event(
|
||||
name='documents_document_new_version', label=_('New version uploaded')
|
||||
)
|
||||
event_document_version_revert = Event(
|
||||
name='documents_document_version_revert',
|
||||
label=_('Document version reverted')
|
||||
)
|
||||
|
||||
@@ -71,7 +71,8 @@ class DocumentForm(forms.ModelForm):
|
||||
self.fields['document_type_available_filenames'] = forms.ModelChoiceField(
|
||||
queryset=filenames_qs,
|
||||
required=False,
|
||||
label=_('Quick document rename'))
|
||||
label=_('Quick document rename')
|
||||
)
|
||||
|
||||
|
||||
class DocumentPropertiesForm(DetailForm):
|
||||
@@ -88,7 +89,9 @@ class DocumentTypeSelectForm(forms.Form):
|
||||
Form to select the document type of a document to be created, used
|
||||
as form #1 in the document creation wizard
|
||||
"""
|
||||
document_type = forms.ModelChoiceField(queryset=DocumentType.objects.all(), label=('Document type'))
|
||||
document_type = forms.ModelChoiceField(
|
||||
queryset=DocumentType.objects.all(), label=('Document type')
|
||||
)
|
||||
|
||||
|
||||
class PrintForm(forms.Form):
|
||||
@@ -114,8 +117,15 @@ class DocumentTypeFilenameForm_create(forms.ModelForm):
|
||||
|
||||
|
||||
class DocumentDownloadForm(forms.Form):
|
||||
compressed = forms.BooleanField(label=_('Compress'), required=False, help_text=_('Download the document in the original format or in a compressed manner. This option is selectable only when downloading one document, for multiple documents, the bundle will always be downloads as a compressed file.'))
|
||||
zip_filename = forms.CharField(initial=DEFAULT_ZIP_FILENAME, label=_('Compressed filename'), required=False, help_text=_('The filename of the compressed file that will contain the documents to be downloaded, if the previous option is selected.'))
|
||||
compressed = forms.BooleanField(
|
||||
label=_('Compress'), required=False,
|
||||
help_text=_('Download the document in the original format or in a compressed manner. This option is selectable only when downloading one document, for multiple documents, the bundle will always be downloads as a compressed file.')
|
||||
)
|
||||
zip_filename = forms.CharField(
|
||||
initial=DEFAULT_ZIP_FILENAME, label=_('Compressed filename'),
|
||||
required=False,
|
||||
help_text=_('The filename of the compressed file that will contain the documents to be downloaded, if the previous option is selected.')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.document_versions = kwargs.pop('document_versions', None)
|
||||
|
||||
@@ -18,4 +18,6 @@ class DocumentMetadataHelper(object):
|
||||
try:
|
||||
return self.instance.metadata.get(metadata_type__name=name).value
|
||||
except MetadataType.DoesNotExist:
|
||||
raise AttributeError(_('\'metadata\' object has no attribute \'%s\'') % name)
|
||||
raise AttributeError(
|
||||
_('\'metadata\' object has no attribute \'%s\'') % name
|
||||
)
|
||||
|
||||
@@ -12,17 +12,61 @@ from .permissions import (
|
||||
permission_metadata_type_edit, permission_metadata_type_view
|
||||
)
|
||||
|
||||
link_documents_missing_required_metadata = Link(icon='fa fa-edit', text=_('Missing metadata'), view='metadata:documents_missing_required_metadata')
|
||||
link_metadata_add = Link(permissions=[permission_metadata_document_add], text=_('Add metadata'), view='metadata:metadata_add', args='object.pk')
|
||||
link_metadata_edit = Link(permissions=[permission_metadata_document_edit], text=_('Edit metadata'), view='metadata:metadata_edit', args='object.pk')
|
||||
link_metadata_multiple_add = Link(permissions=[permission_metadata_document_add], text=_('Add metadata'), view='metadata:metadata_multiple_add')
|
||||
link_metadata_multiple_edit = Link(permissions=[permission_metadata_document_edit], text=_('Edit metadata'), view='metadata:metadata_multiple_edit')
|
||||
link_metadata_multiple_remove = Link(permissions=[permission_metadata_document_remove], text=_('Remove metadata'), view='metadata:metadata_multiple_remove')
|
||||
link_metadata_remove = Link(permissions=[permission_metadata_document_remove], text=_('Remove metadata'), view='metadata:metadata_remove', args='object.pk')
|
||||
link_metadata_view = Link(permissions=[permission_metadata_document_view], text=_('Metadata'), view='metadata:metadata_view', args='object.pk')
|
||||
link_setup_document_type_metadata = Link(permissions=[permission_document_type_edit], text=_('Optional metadata'), view='metadata:setup_document_type_metadata', args='resolved_object.pk')
|
||||
link_setup_document_type_metadata_required = Link(permissions=[permission_document_type_edit], text=_('Required metadata'), view='metadata:setup_document_type_metadata_required', args='resolved_object.pk')
|
||||
link_setup_metadata_type_create = Link(permissions=[permission_metadata_type_create], text=_('Create new'), view='metadata:setup_metadata_type_create')
|
||||
link_setup_metadata_type_delete = Link(permissions=[permission_metadata_type_delete], tags='dangerous', text=_('Delete'), view='metadata:setup_metadata_type_delete', args='object.pk')
|
||||
link_setup_metadata_type_edit = Link(permissions=[permission_metadata_type_edit], text=_('Edit'), view='metadata:setup_metadata_type_edit', args='object.pk')
|
||||
link_setup_metadata_type_list = Link(icon='fa fa-pencil', permissions=[permission_metadata_type_view], text=_('Metadata types'), view='metadata:setup_metadata_type_list')
|
||||
link_documents_missing_required_metadata = Link(
|
||||
icon='fa fa-edit', text=_('Missing metadata'),
|
||||
view='metadata:documents_missing_required_metadata'
|
||||
)
|
||||
link_metadata_add = Link(
|
||||
permissions=[permission_metadata_document_add], text=_('Add metadata'),
|
||||
view='metadata:metadata_add', args='object.pk'
|
||||
)
|
||||
link_metadata_edit = Link(
|
||||
permissions=[permission_metadata_document_edit], text=_('Edit metadata'),
|
||||
view='metadata:metadata_edit', args='object.pk'
|
||||
)
|
||||
link_metadata_multiple_add = Link(
|
||||
permissions=[permission_metadata_document_add], text=_('Add metadata'),
|
||||
view='metadata:metadata_multiple_add'
|
||||
)
|
||||
link_metadata_multiple_edit = Link(
|
||||
permissions=[permission_metadata_document_edit], text=_('Edit metadata'),
|
||||
view='metadata:metadata_multiple_edit'
|
||||
)
|
||||
link_metadata_multiple_remove = Link(
|
||||
permissions=[permission_metadata_document_remove],
|
||||
text=_('Remove metadata'), view='metadata:metadata_multiple_remove'
|
||||
)
|
||||
link_metadata_remove = Link(
|
||||
permissions=[permission_metadata_document_remove],
|
||||
text=_('Remove metadata'), view='metadata:metadata_remove',
|
||||
args='object.pk'
|
||||
)
|
||||
link_metadata_view = Link(
|
||||
permissions=[permission_metadata_document_view], text=_('Metadata'),
|
||||
view='metadata:metadata_view', args='object.pk'
|
||||
)
|
||||
link_setup_document_type_metadata = Link(
|
||||
permissions=[permission_document_type_edit], text=_('Optional metadata'),
|
||||
view='metadata:setup_document_type_metadata', args='resolved_object.pk'
|
||||
)
|
||||
link_setup_document_type_metadata_required = Link(
|
||||
permissions=[permission_document_type_edit], text=_('Required metadata'),
|
||||
view='metadata:setup_document_type_metadata_required',
|
||||
args='resolved_object.pk'
|
||||
)
|
||||
link_setup_metadata_type_create = Link(
|
||||
permissions=[permission_metadata_type_create], text=_('Create new'),
|
||||
view='metadata:setup_metadata_type_create'
|
||||
)
|
||||
link_setup_metadata_type_delete = Link(
|
||||
permissions=[permission_metadata_type_delete], tags='dangerous',
|
||||
text=_('Delete'), view='metadata:setup_metadata_type_delete',
|
||||
args='object.pk')
|
||||
link_setup_metadata_type_edit = Link(
|
||||
permissions=[permission_metadata_type_edit], text=_('Edit'),
|
||||
view='metadata:setup_metadata_type_edit', args='object.pk'
|
||||
)
|
||||
link_setup_metadata_type_list = Link(
|
||||
icon='fa fa-pencil', permissions=[permission_metadata_type_view],
|
||||
text=_('Metadata types'), view='metadata:setup_metadata_type_list'
|
||||
)
|
||||
|
||||
@@ -5,13 +5,29 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from permissions import PermissionNamespace
|
||||
|
||||
namespace = PermissionNamespace('metadata', _('Metadata'))
|
||||
permission_metadata_document_edit = namespace.add_permission(name='metadata_document_edit', label=_('Edit a document\'s metadata'))
|
||||
permission_metadata_document_add = namespace.add_permission(name='metadata_document_add', label=_('Add metadata to a document'))
|
||||
permission_metadata_document_remove = namespace.add_permission(name='metadata_document_remove', label=_('Remove metadata from a document'))
|
||||
permission_metadata_document_view = namespace.add_permission(name='metadata_document_view', label=_('View metadata from a document'))
|
||||
permission_metadata_document_edit = namespace.add_permission(
|
||||
name='metadata_document_edit', label=_('Edit a document\'s metadata')
|
||||
)
|
||||
permission_metadata_document_add = namespace.add_permission(
|
||||
name='metadata_document_add', label=_('Add metadata to a document'))
|
||||
permission_metadata_document_remove = namespace.add_permission(
|
||||
name='metadata_document_remove',
|
||||
label=_('Remove metadata from a document')
|
||||
)
|
||||
permission_metadata_document_view = namespace.add_permission(
|
||||
name='metadata_document_view', label=_('View metadata from a document')
|
||||
)
|
||||
|
||||
setup_namespace = PermissionNamespace('metadata_setup', _('Metadata setup'))
|
||||
permission_metadata_type_edit = setup_namespace.add_permission(name='metadata_type_edit', label=_('Edit metadata types'))
|
||||
permission_metadata_type_create = setup_namespace.add_permission(name='metadata_type_create', label=_('Create new metadata types'))
|
||||
permission_metadata_type_delete = setup_namespace.add_permission(name='metadata_type_delete', label=_('Delete metadata types'))
|
||||
permission_metadata_type_view = setup_namespace.add_permission(name='metadata_type_view', label=_('View metadata types'))
|
||||
permission_metadata_type_edit = setup_namespace.add_permission(
|
||||
name='metadata_type_edit', label=_('Edit metadata types')
|
||||
)
|
||||
permission_metadata_type_create = setup_namespace.add_permission(
|
||||
name='metadata_type_create', label=_('Create new metadata types')
|
||||
)
|
||||
permission_metadata_type_delete = setup_namespace.add_permission(
|
||||
name='metadata_type_delete', label=_('Delete metadata types')
|
||||
)
|
||||
permission_metadata_type_view = setup_namespace.add_permission(
|
||||
name='metadata_type_view', label=_('View metadata types')
|
||||
)
|
||||
|
||||
@@ -16,7 +16,9 @@ from .settings import UNPAPER_PATH
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
UNPAPER = sh.Command(UNPAPER_PATH).bake(overwrite=True, no_multi_pages=True)
|
||||
UNPAPER = sh.Command(UNPAPER_PATH).bake(
|
||||
overwrite=True, no_multi_pages=True
|
||||
)
|
||||
except sh.CommandNotFound:
|
||||
logger.debug('unpaper not found')
|
||||
UNPAPER = None
|
||||
@@ -37,7 +39,9 @@ def execute_unpaper(input_filepath, output_filepath=None):
|
||||
"""
|
||||
if UNPAPER:
|
||||
if not output_filepath:
|
||||
fd, output_filepath = tempfile.mkstemp(dir=setting_temporary_directory.value)
|
||||
fd, output_filepath = tempfile.mkstemp(
|
||||
dir=setting_temporary_directory.value
|
||||
)
|
||||
|
||||
try:
|
||||
UNPAPER(input_filepath, output_filepath)
|
||||
|
||||
@@ -24,15 +24,24 @@ class DocumentVersionOCRView(generics.GenericAPIView):
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Submit document version for OCR."""
|
||||
|
||||
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
|
||||
serializer = self.get_serializer(
|
||||
data=request.DATA, files=request.FILES
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
document_version = get_object_or_404(DocumentVersion, pk=serializer.data['document_version_id'])
|
||||
document_version = get_object_or_404(
|
||||
DocumentVersion, pk=serializer.data['document_version_id']
|
||||
)
|
||||
|
||||
try:
|
||||
Permission.check_permissions(request.user, [permission_ocr_document])
|
||||
Permission.check_permissions(
|
||||
request.user, [permission_ocr_document]
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(permission_ocr_document, request.user, document_version.document)
|
||||
AccessControlList.objects.check_access(
|
||||
permission_ocr_document, request.user,
|
||||
document_version.document
|
||||
)
|
||||
|
||||
document_version.submit_for_ocr()
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@ from .links import (
|
||||
)
|
||||
from .models import DocumentVersionOCRError
|
||||
from .permissions import permission_ocr_document, permission_ocr_content_view
|
||||
from .settings import setting_pdftotext_path, setting_tesseract_path, setting_unpaper_path
|
||||
from .settings import (
|
||||
setting_pdftotext_path, setting_tesseract_path, setting_unpaper_path
|
||||
)
|
||||
from .tasks import task_do_ocr
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -56,7 +58,9 @@ class OCRApp(MayanAppConfig):
|
||||
APIEndPoint('ocr')
|
||||
|
||||
Document.add_to_class('submit_for_ocr', document_ocr_submit)
|
||||
DocumentVersion.add_to_class('submit_for_ocr', document_version_ocr_submit)
|
||||
DocumentVersion.add_to_class(
|
||||
'submit_for_ocr', document_version_ocr_submit
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=Document, permissions=(
|
||||
@@ -64,9 +68,20 @@ class OCRApp(MayanAppConfig):
|
||||
)
|
||||
)
|
||||
|
||||
SourceColumn(source=DocumentVersionOCRError, label=_('Document'), attribute=encapsulate(lambda entry: document_link(entry.document_version.document)))
|
||||
SourceColumn(source=DocumentVersionOCRError, label=_('Added'), attribute='datetime_submitted')
|
||||
SourceColumn(source=DocumentVersionOCRError, label=_('Result'), attribute='result')
|
||||
SourceColumn(
|
||||
source=DocumentVersionOCRError, label=_('Document'),
|
||||
attribute=encapsulate(
|
||||
lambda entry: document_link(entry.document_version.document)
|
||||
)
|
||||
)
|
||||
SourceColumn(
|
||||
source=DocumentVersionOCRError, label=_('Added'),
|
||||
attribute='datetime_submitted'
|
||||
)
|
||||
SourceColumn(
|
||||
source=DocumentVersionOCRError, label=_('Result'),
|
||||
attribute='result'
|
||||
)
|
||||
|
||||
app.conf.CELERY_QUEUES.append(
|
||||
Queue('ocr', Exchange('ocr'), routing_key='ocr'),
|
||||
@@ -80,45 +95,98 @@ class OCRApp(MayanAppConfig):
|
||||
}
|
||||
)
|
||||
|
||||
document_search.add_model_field(field='versions__pages__ocr_content__content', label=_('Content'))
|
||||
document_search.add_model_field(
|
||||
field='versions__pages__ocr_content__content', label=_('Content')
|
||||
)
|
||||
|
||||
menu_facet.bind_links(links=[link_document_content], sources=[Document])
|
||||
menu_multi_item.bind_links(links=[link_document_submit_multiple], sources=[Document])
|
||||
menu_multi_item.bind_links(links=[link_entry_re_queue_multiple, link_entry_delete_multiple], sources=[DocumentVersionOCRError])
|
||||
menu_object.bind_links(links=[link_document_submit], sources=[Document])
|
||||
menu_object.bind_links(links=[link_entry_re_queue, link_entry_delete], sources=[DocumentVersionOCRError])
|
||||
menu_object.bind_links(links=[link_document_type_ocr_settings], sources=[DocumentType])
|
||||
menu_secondary.bind_links(links=[link_entry_list], sources=['ocr:entry_list', 'ocr:entry_delete_multiple', 'ocr:entry_re_queue_multiple', DocumentVersionOCRError])
|
||||
menu_tools.bind_links(links=[link_document_submit_all, link_entry_list])
|
||||
menu_facet.bind_links(
|
||||
links=[link_document_content], sources=[Document]
|
||||
)
|
||||
menu_multi_item.bind_links(
|
||||
links=[link_document_submit_multiple], sources=[Document]
|
||||
)
|
||||
menu_multi_item.bind_links(
|
||||
links=[link_entry_re_queue_multiple, link_entry_delete_multiple],
|
||||
sources=[DocumentVersionOCRError]
|
||||
)
|
||||
menu_object.bind_links(
|
||||
links=[link_document_submit], sources=[Document]
|
||||
)
|
||||
menu_object.bind_links(
|
||||
links=[link_entry_re_queue, link_entry_delete],\
|
||||
sources=[DocumentVersionOCRError]
|
||||
)
|
||||
menu_object.bind_links(
|
||||
links=[link_document_type_ocr_settings], sources=[DocumentType]
|
||||
)
|
||||
menu_secondary.bind_links(
|
||||
links=[link_entry_list],
|
||||
sources=['ocr:entry_list', 'ocr:entry_delete_multiple', 'ocr:entry_re_queue_multiple', DocumentVersionOCRError]
|
||||
)
|
||||
menu_tools.bind_links(
|
||||
links=[link_document_submit_all, link_entry_list]
|
||||
)
|
||||
|
||||
post_save.connect(initialize_new_ocr_settings, dispatch_uid='initialize_new_ocr_settings', sender=DocumentType)
|
||||
post_version_upload.connect(post_version_upload_ocr, dispatch_uid='post_version_upload_ocr', sender=DocumentVersion)
|
||||
post_save.connect(
|
||||
initialize_new_ocr_settings,
|
||||
dispatch_uid='initialize_new_ocr_settings', sender=DocumentType
|
||||
)
|
||||
post_version_upload.connect(
|
||||
post_version_upload_ocr, dispatch_uid='post_version_upload_ocr',
|
||||
sender=DocumentVersion
|
||||
)
|
||||
|
||||
namespace = PropertyNamespace('ocr', _('OCR'))
|
||||
|
||||
try:
|
||||
pdftotext = sh.Command(setting_pdftotext_path.value)
|
||||
except sh.CommandNotFound:
|
||||
namespace.add_property('pdftotext', _('pdftotext version'), _('not found'), report=True)
|
||||
namespace.add_property(
|
||||
'pdftotext', _('pdftotext version'), _('not found'),
|
||||
report=True
|
||||
)
|
||||
except Exception:
|
||||
namespace.add_property('pdftotext', _('pdftotext version'), _('error getting version'), report=True)
|
||||
namespace.add_property(
|
||||
'pdftotext', _('pdftotext version'),
|
||||
_('error getting version'), report=True
|
||||
)
|
||||
else:
|
||||
namespace.add_property('pdftotext', _('pdftotext version'), pdftotext('-v').stderr, report=True)
|
||||
namespace.add_property(
|
||||
'pdftotext', _('pdftotext version'), pdftotext('-v').stderr,
|
||||
report=True
|
||||
)
|
||||
|
||||
try:
|
||||
tesseract = sh.Command(setting_tesseract_path.value)
|
||||
except sh.CommandNotFound:
|
||||
namespace.add_property('tesseract', _('tesseract version'), _('not found'), report=True)
|
||||
namespace.add_property(
|
||||
'tesseract', _('tesseract version'), _('not found'),
|
||||
report=True
|
||||
)
|
||||
except Exception:
|
||||
namespace.add_property('tesseract', _('tesseract version'), _('error getting version'), report=True)
|
||||
namespace.add_property(
|
||||
'tesseract', _('tesseract version'),
|
||||
_('error getting version'), report=True
|
||||
)
|
||||
else:
|
||||
namespace.add_property('tesseract', _('tesseract version'), tesseract('-v').stderr, report=True)
|
||||
namespace.add_property(
|
||||
'tesseract', _('tesseract version'), tesseract('-v').stderr,
|
||||
report=True
|
||||
)
|
||||
|
||||
try:
|
||||
unpaper = sh.Command(setting_unpaper_path.value)
|
||||
except sh.CommandNotFound:
|
||||
namespace.add_property('unpaper', _('unpaper version'), _('not found'), report=True)
|
||||
namespace.add_property(
|
||||
'unpaper', _('unpaper version'), _('not found'), report=True
|
||||
)
|
||||
except Exception:
|
||||
namespace.add_property('unpaper', _('unpaper version'), _('error getting version'), report=True)
|
||||
namespace.add_property(
|
||||
'unpaper', _('unpaper version'), _('error getting version'),
|
||||
report=True
|
||||
)
|
||||
else:
|
||||
namespace.add_property('unpaper', _('unpaper version'), unpaper('-V').stdout, report=True)
|
||||
namespace.add_property(
|
||||
'unpaper', _('unpaper version'), unpaper('-V').stdout,
|
||||
report=True
|
||||
)
|
||||
|
||||
@@ -20,12 +20,20 @@ class OCRBackendBase(object):
|
||||
|
||||
for page in document_version.pages.all():
|
||||
image = page.get_image()
|
||||
logger.info('Processing page: %d of document version: %s', page.page_number, document_version)
|
||||
logger.info(
|
||||
'Processing page: %d of document version: %s',
|
||||
page.page_number, document_version
|
||||
)
|
||||
document_page_content, created = DocumentPageContent.objects.get_or_create(document_page=page)
|
||||
document_page_content.content = self.execute(file_object=image, language=language)
|
||||
document_page_content.content = self.execute(
|
||||
file_object=image, language=language
|
||||
)
|
||||
document_page_content.save()
|
||||
image.close()
|
||||
logger.info('Finished processing page: %d of document version: %s', page.page_number, document_version)
|
||||
logger.info(
|
||||
'Finished processing page: %d of document version: %s',
|
||||
page.page_number, document_version
|
||||
)
|
||||
|
||||
def execute(self, file_object, language=None, transformations=None):
|
||||
self.language = language
|
||||
|
||||
@@ -33,11 +33,22 @@ class DocumentContentForm(forms.Form):
|
||||
pass
|
||||
else:
|
||||
content.append(conditional_escape(force_unicode(page_content)))
|
||||
content.append('\n\n\n<hr/><div class="document-page-content-divider">- %s -</div><hr/>\n\n\n' % (ugettext('Page %(page_number)d') % {'page_number': page.page_number}))
|
||||
content.append(
|
||||
'\n\n\n<hr/><div class="document-page-content-divider">- %s -</div><hr/>\n\n\n' % (
|
||||
ugettext(
|
||||
'Page %(page_number)d'
|
||||
) % {'page_number': page.page_number}
|
||||
)
|
||||
)
|
||||
|
||||
self.fields['contents'].initial = mark_safe(''.join(content))
|
||||
|
||||
contents = forms.CharField(
|
||||
label=_('Contents'),
|
||||
widget=TextAreaDiv(attrs={'class': 'text_area_div full-height', 'data-height-difference': 360})
|
||||
widget=TextAreaDiv(
|
||||
attrs={
|
||||
'class': 'text_area_div full-height',
|
||||
'data-height-difference': 360
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -9,13 +9,40 @@ from .permissions import (
|
||||
permission_ocr_document_delete, permission_document_type_ocr_setup
|
||||
)
|
||||
|
||||
link_document_content = Link(permissions=[permission_ocr_content_view], text=_('Content'), view='ocr:document_content', args='resolved_object.id')
|
||||
link_document_submit = Link(permissions=[permission_ocr_document], text=_('Submit for OCR'), view='ocr:document_submit', args='object.id')
|
||||
link_document_submit_all = Link(icon='fa fa-font', permissions=[permission_ocr_document], text=_('OCR all documents'), view='ocr:document_submit_all')
|
||||
link_document_submit_multiple = Link(text=_('Submit for OCR'), view='ocr:document_submit_multiple')
|
||||
link_document_type_ocr_settings = Link(permissions=[permission_document_type_ocr_setup], text=_('Setup OCR'), view='ocr:document_type_ocr_settings', args='resolved_object.id')
|
||||
link_entry_delete = Link(permissions=[permission_ocr_document_delete], text=_('Delete'), view='ocr:entry_delete', args='object.id')
|
||||
link_entry_delete_multiple = Link(text=_('Delete'), view='ocr:entry_delete_multiple')
|
||||
link_entry_list = Link(icon='fa fa-file-text-o', permissions=[permission_ocr_document], text=_('OCR errors'), view='ocr:entry_list')
|
||||
link_entry_re_queue = Link(permissions=[permission_ocr_document], text=_('Re-queue'), view='ocr:entry_re_queue', args='object.id')
|
||||
link_entry_re_queue_multiple = Link(text=_('Re-queue'), view='ocr:entry_re_queue_multiple')
|
||||
link_document_content = Link(
|
||||
permissions=[permission_ocr_content_view], text=_('Content'),
|
||||
view='ocr:document_content', args='resolved_object.id'
|
||||
)
|
||||
link_document_submit = Link(
|
||||
permissions=[permission_ocr_document], text=_('Submit for OCR'),
|
||||
view='ocr:document_submit', args='object.id'
|
||||
)
|
||||
link_document_submit_all = Link(
|
||||
icon='fa fa-font', permissions=[permission_ocr_document],
|
||||
text=_('OCR all documents'), view='ocr:document_submit_all'
|
||||
)
|
||||
link_document_submit_multiple = Link(
|
||||
text=_('Submit for OCR'), view='ocr:document_submit_multiple'
|
||||
)
|
||||
link_document_type_ocr_settings = Link(
|
||||
permissions=[permission_document_type_ocr_setup], text=_('Setup OCR'),
|
||||
view='ocr:document_type_ocr_settings', args='resolved_object.id'
|
||||
)
|
||||
link_entry_delete = Link(
|
||||
permissions=[permission_ocr_document_delete], text=_('Delete'),
|
||||
view='ocr:entry_delete', args='object.id'
|
||||
)
|
||||
link_entry_delete_multiple = Link(
|
||||
text=_('Delete'), view='ocr:entry_delete_multiple'
|
||||
)
|
||||
link_entry_list = Link(
|
||||
icon='fa fa-file-text-o', permissions=[permission_ocr_document],
|
||||
text=_('OCR errors'), view='ocr:entry_list'
|
||||
)
|
||||
link_entry_re_queue = Link(
|
||||
permissions=[permission_ocr_document], text=_('Re-queue'),
|
||||
view='ocr:entry_re_queue', args='object.id'
|
||||
)
|
||||
link_entry_re_queue_multiple = Link(
|
||||
text=_('Re-queue'), view='ocr:entry_re_queue_multiple'
|
||||
)
|
||||
|
||||
@@ -11,8 +11,14 @@ class DocumentTypeSettings(models.Model):
|
||||
"""
|
||||
Define for OCR for a specific document should behave
|
||||
"""
|
||||
document_type = models.OneToOneField(DocumentType, related_name='ocr_settings', unique=True, verbose_name=_('Document type'))
|
||||
auto_ocr = models.BooleanField(default=True, verbose_name=_('Automatically queue newly created documents for OCR.'))
|
||||
document_type = models.OneToOneField(
|
||||
DocumentType, related_name='ocr_settings', unique=True,
|
||||
verbose_name=_('Document type')
|
||||
)
|
||||
auto_ocr = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_('Automatically queue newly created documents for OCR.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Document type settings')
|
||||
@@ -21,8 +27,12 @@ class DocumentTypeSettings(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DocumentVersionOCRError(models.Model):
|
||||
document_version = models.ForeignKey(DocumentVersion, verbose_name=_('Document version'))
|
||||
datetime_submitted = models.DateTimeField(auto_now=True, db_index=True, verbose_name=_('Date time submitted'))
|
||||
document_version = models.ForeignKey(
|
||||
DocumentVersion, verbose_name=_('Document version')
|
||||
)
|
||||
datetime_submitted = models.DateTimeField(
|
||||
auto_now=True, db_index=True, verbose_name=_('Date time submitted')
|
||||
)
|
||||
result = models.TextField(blank=True, null=True, verbose_name=_('Result'))
|
||||
|
||||
def __str__(self):
|
||||
@@ -39,7 +49,10 @@ class DocumentPageContent(models.Model):
|
||||
"""
|
||||
Model that describes a document page content
|
||||
"""
|
||||
document_page = models.OneToOneField(DocumentPage, related_name='ocr_content', verbose_name=_('Document page'))
|
||||
document_page = models.OneToOneField(
|
||||
DocumentPage, related_name='ocr_content',
|
||||
verbose_name=_('Document page')
|
||||
)
|
||||
content = models.TextField(blank=True, verbose_name=_('Content'))
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -24,10 +24,13 @@ def register_parser(mimetypes, parsers):
|
||||
try:
|
||||
parser_instance = parser()
|
||||
except ParserError:
|
||||
# If parser fails initialization is not added to the list for this mimetype
|
||||
# If parser fails initialization is not added to the list for
|
||||
# this mimetype
|
||||
pass
|
||||
else:
|
||||
mimetype_registry.setdefault(mimetype, []).append(parser_instance)
|
||||
mimetype_registry.setdefault(mimetype, []).append(
|
||||
parser_instance
|
||||
)
|
||||
|
||||
|
||||
def parse_document_page(document_page, descriptor=None, mimetype=None):
|
||||
@@ -66,7 +69,10 @@ class Parser(object):
|
||||
"""
|
||||
|
||||
def parse(self, document_page, descriptor=None):
|
||||
raise NotImplementedError('Your %s class has not defined a parse() method, which is required.', self.__class__.__name__)
|
||||
raise NotImplementedError(
|
||||
'Your %s class has not defined a parse() method, which is required.',
|
||||
self.__class__.__name__
|
||||
)
|
||||
|
||||
|
||||
class SlateParser(Parser):
|
||||
@@ -105,11 +111,15 @@ class PopplerParser(Parser):
|
||||
pagenum = str(document_page.page_number)
|
||||
|
||||
if descriptor:
|
||||
destination_descriptor, temp_filepath = tempfile.mkstemp(dir=setting_temporary_directory.value)
|
||||
destination_descriptor, temp_filepath = tempfile.mkstemp(
|
||||
dir=setting_temporary_directory.value
|
||||
)
|
||||
copyfile(descriptor, temp_filepath)
|
||||
document_file = temp_filepath
|
||||
else:
|
||||
document_file = document_page.document.document_save_to_temp_dir(document_page.document.checksum)
|
||||
document_file = document_page.document.document_save_to_temp_dir(
|
||||
document_page.document.checksum
|
||||
)
|
||||
|
||||
logger.debug('document_file: %s', document_file)
|
||||
|
||||
@@ -124,7 +134,10 @@ class PopplerParser(Parser):
|
||||
command.append(document_file)
|
||||
command.append('-')
|
||||
|
||||
proc = subprocess.Popen(command, close_fds=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
proc = subprocess.Popen(
|
||||
command, close_fds=True, stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE
|
||||
)
|
||||
return_code = proc.wait()
|
||||
if return_code != 0:
|
||||
logger.error(proc.stderr.readline())
|
||||
@@ -139,4 +152,6 @@ class PopplerParser(Parser):
|
||||
document_page.save()
|
||||
|
||||
|
||||
register_parser(mimetypes=['application/pdf'], parsers=[PopplerParser, SlateParser])
|
||||
register_parser(
|
||||
mimetypes=['application/pdf'], parsers=[PopplerParser, SlateParser]
|
||||
)
|
||||
|
||||
@@ -6,7 +6,17 @@ from permissions import PermissionNamespace
|
||||
|
||||
namespace = PermissionNamespace('ocr', _('OCR'))
|
||||
|
||||
permission_ocr_document = namespace.add_permission(name='ocr_document', label=_('Submit documents for OCR'))
|
||||
permission_ocr_document_delete = namespace.add_permission(name='ocr_document_delete', label=_('Delete documents from OCR queue'))
|
||||
permission_ocr_content_view = namespace.add_permission(name='ocr_content_view', label=_('Can view the transcribed text from document'))
|
||||
permission_document_type_ocr_setup = namespace.add_permission(name='ocr_document_type_setup', label=_('Change document type OCR settings'))
|
||||
permission_ocr_document = namespace.add_permission(
|
||||
name='ocr_document', label=_('Submit documents for OCR')
|
||||
)
|
||||
permission_ocr_document_delete = namespace.add_permission(
|
||||
name='ocr_document_delete', label=_('Delete documents from OCR queue')
|
||||
)
|
||||
permission_ocr_content_view = namespace.add_permission(
|
||||
name='ocr_content_view',
|
||||
label=_('Can view the transcribed text from document')
|
||||
)
|
||||
permission_document_type_ocr_setup = namespace.add_permission(
|
||||
name='ocr_document_type_setup',
|
||||
label=_('Change document type OCR settings')
|
||||
)
|
||||
|
||||
@@ -5,7 +5,20 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from smart_settings import Namespace
|
||||
|
||||
namespace = Namespace(name='ocr', label=_('OCR'))
|
||||
setting_tesseract_path = namespace.add_setting(global_name='OCR_TESSERACT_PATH', default='/usr/bin/tesseract', help_text=_('File path to tesseract program.'), is_path=True)
|
||||
setting_unpaper_path = namespace.add_setting(global_name='OCR_UNPAPER_PATH', default='/usr/bin/unpaper', help_text=_('File path to unpaper program.'), is_path=True)
|
||||
setting_pdftotext_path = namespace.add_setting(global_name='OCR_PDFTOTEXT_PATH', default='/usr/bin/pdftotext', help_text=_('File path to poppler\'s pdftotext program used to extract text from PDF files.'), is_path=True)
|
||||
setting_ocr_backend = namespace.add_setting(global_name='OCR_BACKEND', default='ocr.backends.tesseract.Tesseract', help_text=_('Full path to the backend to be used to do OCR.'))
|
||||
setting_tesseract_path = namespace.add_setting(
|
||||
global_name='OCR_TESSERACT_PATH', default='/usr/bin/tesseract',
|
||||
help_text=_('File path to tesseract program.'), is_path=True
|
||||
)
|
||||
setting_unpaper_path = namespace.add_setting(
|
||||
global_name='OCR_UNPAPER_PATH', default='/usr/bin/unpaper',
|
||||
help_text=_('File path to unpaper program.'), is_path=True
|
||||
)
|
||||
setting_pdftotext_path = namespace.add_setting(
|
||||
global_name='OCR_PDFTOTEXT_PATH', default='/usr/bin/pdftotext',
|
||||
help_text=_('File path to poppler\'s pdftotext program used to extract text from PDF files.'),
|
||||
is_path=True
|
||||
)
|
||||
setting_ocr_backend = namespace.add_setting(
|
||||
global_name='OCR_BACKEND', default='ocr.backends.tesseract.Tesseract',
|
||||
help_text=_('Full path to the backend to be used to do OCR.')
|
||||
)
|
||||
|
||||
@@ -2,4 +2,6 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.dispatch import Signal
|
||||
|
||||
post_document_version_ocr = Signal(providing_args=['instance'], use_caching=True)
|
||||
post_document_version_ocr = Signal(
|
||||
providing_args=['instance'], use_caching=True
|
||||
)
|
||||
|
||||
@@ -66,4 +66,3 @@ def task_do_ocr(self, document_version_pk):
|
||||
lock.release()
|
||||
except LockError:
|
||||
logger.debug('unable to obtain lock: %s' % lock_id)
|
||||
pass
|
||||
|
||||
@@ -16,19 +16,25 @@ class DocumentOCRTestCase(TestCase):
|
||||
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
|
||||
|
||||
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||
self.document = self.document_type.new_document(file_object=File(file_object), label='small document')
|
||||
self.document = self.document_type.new_document(
|
||||
file_object=File(file_object), label='small document'
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.document.delete()
|
||||
self.document_type.delete()
|
||||
|
||||
def test_ocr_language_backends_end(self):
|
||||
self.assertTrue('Mayan EDMS Documentation' in self.document.pages.first().ocr_content.content)
|
||||
self.assertTrue(
|
||||
'Mayan EDMS Documentation' in self.document.pages.first().ocr_content.content
|
||||
)
|
||||
|
||||
|
||||
class GermanOCRSupportTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
|
||||
self.document_type = DocumentType.objects.create(
|
||||
label=TEST_DOCUMENT_TYPE
|
||||
)
|
||||
|
||||
# Get corresponding language code for German from the default language
|
||||
# choices list
|
||||
@@ -37,12 +43,18 @@ class GermanOCRSupportTestCase(TestCase):
|
||||
self.assertEqual('deu', language_code)
|
||||
|
||||
with open(TEST_DEU_DOCUMENT_PATH) as file_object:
|
||||
self.document = self.document_type.new_document(file_object=File(file_object), language=language_code)
|
||||
self.document = self.document_type.new_document(
|
||||
file_object=File(file_object), language=language_code
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.document.delete()
|
||||
self.document_type.delete()
|
||||
|
||||
def test_ocr_language_backends_end(self):
|
||||
self.assertTrue('Repository für elektronische Dokumente.' in self.document.pages.first().ocr_content.content)
|
||||
self.assertTrue('Es bietet einen elektronischen Tresor oder' in self.document.pages.first().ocr_content.content)
|
||||
self.assertTrue(
|
||||
'Repository für elektronische Dokumente.' in self.document.pages.first().ocr_content.content
|
||||
)
|
||||
self.assertTrue(
|
||||
'Es bietet einen elektronischen Tresor oder' in self.document.pages.first().ocr_content.content
|
||||
)
|
||||
|
||||
@@ -10,20 +10,45 @@ from .views import (
|
||||
|
||||
urlpatterns = patterns(
|
||||
'ocr.views',
|
||||
url(r'^(?P<document_id>\d+)/content/$', 'document_content', name='document_content'),
|
||||
url(r'^document/(?P<pk>\d+)/submit/$', DocumentSubmitView.as_view(), name='document_submit'),
|
||||
url(r'^document/all/submit/$', DocumentAllSubmitView.as_view(), name='document_submit_all'),
|
||||
url(r'^document/multiple/submit/$', DocumentManySubmitView.as_view(), name='document_submit_multiple'),
|
||||
url(r'^document_type/(?P<pk>\d+)/ocr/settings/$', DocumentTypeSettingsEditView.as_view(), name='document_type_ocr_settings'),
|
||||
url(
|
||||
r'^(?P<document_id>\d+)/content/$', 'document_content',
|
||||
name='document_content'
|
||||
),
|
||||
url(
|
||||
r'^document/(?P<pk>\d+)/submit/$', DocumentSubmitView.as_view(),
|
||||
name='document_submit'
|
||||
),
|
||||
url(
|
||||
r'^document/all/submit/$', DocumentAllSubmitView.as_view(),
|
||||
name='document_submit_all'
|
||||
),
|
||||
url(
|
||||
r'^document/multiple/submit/$', DocumentManySubmitView.as_view(),
|
||||
name='document_submit_multiple'
|
||||
),
|
||||
url(
|
||||
r'^document_type/(?P<pk>\d+)/ocr/settings/$',
|
||||
DocumentTypeSettingsEditView.as_view(),
|
||||
name='document_type_ocr_settings'
|
||||
),
|
||||
|
||||
url(r'^all/$', 'entry_list', name='entry_list'),
|
||||
url(r'^(?P<pk>\d+)/delete/$', 'entry_delete', name='entry_delete'),
|
||||
url(r'^multiple/delete/$', 'entry_delete_multiple', name='entry_delete_multiple'),
|
||||
url(
|
||||
r'^multiple/delete/$', 'entry_delete_multiple',
|
||||
name='entry_delete_multiple'
|
||||
),
|
||||
url(r'^(?P<pk>\d+)/re-queue/$', 'entry_re_queue', name='entry_re_queue'),
|
||||
url(r'^multiple/re-queue/$', 'entry_re_queue_multiple', name='entry_re_queue_multiple'),
|
||||
url(
|
||||
r'^multiple/re-queue/$', 'entry_re_queue_multiple',
|
||||
name='entry_re_queue_multiple'
|
||||
),
|
||||
)
|
||||
|
||||
api_urls = patterns(
|
||||
'',
|
||||
url(r'^submit/$', DocumentVersionOCRView.as_view(), name='document-version-ocr-submit-view'),
|
||||
url(
|
||||
r'^submit/$', DocumentVersionOCRView.as_view(),
|
||||
name='document-version-ocr-submit-view'
|
||||
),
|
||||
)
|
||||
|
||||
@@ -33,13 +33,20 @@ class DocumentSubmitView(ConfirmView):
|
||||
document = obj
|
||||
|
||||
try:
|
||||
Permission.check_permissions(request.user, [permission_ocr_document])
|
||||
Permission.check_permissions(
|
||||
request.user, [permission_ocr_document]
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(permission_ocr_document, request.user, document)
|
||||
AccessControlList.objects.check_access(
|
||||
permission_ocr_document, request.user, document
|
||||
)
|
||||
|
||||
document.submit_for_ocr()
|
||||
messages.success(request, _('Document: %(document)s was added to the OCR queue.') % {
|
||||
'document': document}
|
||||
messages.success(
|
||||
request,
|
||||
_('Document: %(document)s was added to the OCR queue.') % {
|
||||
'document': document
|
||||
}
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -58,7 +65,9 @@ class DocumentAllSubmitView(ConfirmView):
|
||||
document.submit_for_ocr()
|
||||
count += 1
|
||||
|
||||
messages.success(request, _('%d documents added to the OCR queue.') % count)
|
||||
messages.success(
|
||||
request, _('%d documents added to the OCR queue.') % count
|
||||
)
|
||||
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
@@ -82,14 +91,20 @@ class DocumentTypeSettingsEditView(SingleObjectEditView):
|
||||
view_permission = permission_document_type_ocr_setup
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(DocumentType, pk=self.kwargs['pk']).ocr_settings
|
||||
return get_object_or_404(
|
||||
DocumentType, pk=self.kwargs['pk']
|
||||
).ocr_settings
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DocumentTypeSettingsEditView, self).get_context_data(**kwargs)
|
||||
context = super(
|
||||
DocumentTypeSettingsEditView, self
|
||||
).get_context_data(**kwargs)
|
||||
|
||||
context.update(
|
||||
{
|
||||
'title': _('Edit OCR settings for document type: %s') % self.get_object().document_type
|
||||
'title': _(
|
||||
'Edit OCR settings for document type: %s'
|
||||
) % self.get_object().document_type
|
||||
}
|
||||
)
|
||||
|
||||
@@ -100,9 +115,13 @@ def document_content(request, document_id):
|
||||
document = get_object_or_404(Document, pk=document_id)
|
||||
|
||||
try:
|
||||
Permission.check_permissions(request.user, [permission_ocr_content_view])
|
||||
Permission.check_permissions(
|
||||
request.user, [permission_ocr_content_view]
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(permission_ocr_content_view, request.user, document)
|
||||
AccessControlList.objects.check_access(
|
||||
permission_ocr_content_view, request.user, document
|
||||
)
|
||||
|
||||
document.add_as_recent_document_for_user(request.user)
|
||||
|
||||
@@ -132,7 +151,9 @@ def entry_list(request):
|
||||
|
||||
|
||||
def entry_delete(request, pk=None, pk_list=None):
|
||||
Permission.check_permissions(request.user, [permission_ocr_document_delete])
|
||||
Permission.check_permissions(
|
||||
request.user, [permission_ocr_document_delete]
|
||||
)
|
||||
|
||||
if pk:
|
||||
entries = [get_object_or_404(DocumentVersionOCRError, pk=pk)]
|
||||
@@ -140,7 +161,11 @@ def entry_delete(request, pk=None, pk_list=None):
|
||||
entries = [get_object_or_404(DocumentVersionOCRError, pk=pk) for pk in pk_list.split(',')]
|
||||
else:
|
||||
messages.error(request, _('Make at least one selection.'))
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))
|
||||
return HttpResponseRedirect(
|
||||
request.META.get(
|
||||
'HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)
|
||||
)
|
||||
)
|
||||
|
||||
next = request.POST.get('next', request.GET.get('next', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
@@ -149,12 +174,18 @@ def entry_delete(request, pk=None, pk_list=None):
|
||||
for entry in entries:
|
||||
try:
|
||||
entry.delete()
|
||||
messages.success(request, _('Entry: %(entry)s deleted successfully.') % {
|
||||
'entry': entry})
|
||||
messages.success(
|
||||
request, _('Entry: %(entry)s deleted successfully.') % {
|
||||
'entry': entry
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as exception:
|
||||
messages.error(request, _('Error entry: %(entry)s; %(error)s') % {
|
||||
'entry': entry, 'error': exception})
|
||||
messages.error(
|
||||
request, _('Error entry: %(entry)s; %(error)s') % {
|
||||
'entry': entry, 'error': exception
|
||||
}
|
||||
)
|
||||
return HttpResponseRedirect(next)
|
||||
|
||||
context = {
|
||||
|
||||
@@ -26,9 +26,22 @@ class PermissionsApp(MayanAppConfig):
|
||||
|
||||
APIEndPoint('permissions')
|
||||
|
||||
menu_object.bind_links(links=[link_role_edit, link_role_members, link_role_permissions, link_role_delete], sources=[Role])
|
||||
menu_multi_item.bind_links(links=[link_permission_grant, link_permission_revoke], sources=['permissions:role_permissions'])
|
||||
menu_secondary.bind_links(links=[link_role_list, link_role_create], sources=[Role, 'permissions:role_create', 'permissions:role_list'])
|
||||
menu_object.bind_links(
|
||||
links=[
|
||||
link_role_edit, link_role_members, link_role_permissions,
|
||||
link_role_delete
|
||||
], sources=[Role]
|
||||
)
|
||||
menu_multi_item.bind_links(
|
||||
links=[link_permission_grant, link_permission_revoke],
|
||||
sources=['permissions:role_permissions']
|
||||
)
|
||||
menu_secondary.bind_links(
|
||||
links=[link_role_list, link_role_create],
|
||||
sources=[Role, 'permissions:role_create', 'permissions:role_list']
|
||||
)
|
||||
menu_setup.bind_links(links=[link_role_list])
|
||||
|
||||
perform_upgrade.connect(purge_permissions, dispatch_uid='purge_permissions')
|
||||
perform_upgrade.connect(
|
||||
purge_permissions, dispatch_uid='purge_permissions'
|
||||
)
|
||||
|
||||
@@ -23,7 +23,9 @@ class PermissionNamespace(object):
|
||||
try:
|
||||
return cls._registry[name]
|
||||
except KeyError:
|
||||
raise InvalidNamespace('Invalid namespace name. This is probably an obsolete permission namespace, execute the management command "purge_permissions" and try again.')
|
||||
raise InvalidNamespace(
|
||||
'Invalid namespace name. This is probably an obsolete permission namespace, execute the management command "purge_permissions" and try again.'
|
||||
)
|
||||
|
||||
def __init__(self, name, label):
|
||||
self.name = name
|
||||
@@ -65,7 +67,9 @@ class Permission(object):
|
||||
@classmethod
|
||||
def all(cls):
|
||||
# Return sorted permisions by namespace.name
|
||||
return sorted(cls._permissions.values(), key=lambda x: x.namespace.name)
|
||||
return sorted(
|
||||
cls._permissions.values(), key=lambda x: x.namespace.name
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get(cls, get_dict, proxy_only=False):
|
||||
|
||||
@@ -10,11 +10,36 @@ from .permissions import (
|
||||
permission_role_view
|
||||
)
|
||||
|
||||
link_permission_grant = Link(permissions=[permission_permission_grant], text=_('Grant'), view='permissions:permission_multiple_grant')
|
||||
link_permission_revoke = Link(permissions=[permission_permission_revoke], text=_('Revoke'), view='permissions:permission_multiple_revoke')
|
||||
link_role_create = Link(permissions=[permission_role_create], text=_('Create new role'), view='permissions:role_create')
|
||||
link_role_delete = Link(permissions=[permission_role_delete], tags='dangerous', text=_('Delete'), view='permissions:role_delete', args='object.id')
|
||||
link_role_edit = Link(permissions=[permission_role_edit], text=_('Edit'), view='permissions:role_edit', args='object.id')
|
||||
link_role_list = Link(icon='fa fa-user-secret', permissions=[permission_role_view], text=_('Roles'), view='permissions:role_list')
|
||||
link_role_members = Link(permissions=[permission_role_edit], text=_('Members'), view='permissions:role_members', args='object.id')
|
||||
link_role_permissions = Link(permissions=[permission_permission_grant, permission_permission_revoke], text=_('Role permissions'), view='permissions:role_permissions', args='object.id')
|
||||
link_permission_grant = Link(
|
||||
permissions=[permission_permission_grant], text=_('Grant'),
|
||||
view='permissions:permission_multiple_grant'
|
||||
)
|
||||
link_permission_revoke = Link(
|
||||
permissions=[permission_permission_revoke], text=_('Revoke'),
|
||||
view='permissions:permission_multiple_revoke'
|
||||
)
|
||||
link_role_create = Link(
|
||||
permissions=[permission_role_create], text=_('Create new role'),
|
||||
view='permissions:role_create'
|
||||
)
|
||||
link_role_delete = Link(
|
||||
permissions=[permission_role_delete], tags='dangerous', text=_('Delete'),
|
||||
view='permissions:role_delete', args='object.id'
|
||||
)
|
||||
link_role_edit = Link(
|
||||
permissions=[permission_role_edit], text=_('Edit'),
|
||||
view='permissions:role_edit', args='object.id'
|
||||
)
|
||||
link_role_list = Link(
|
||||
icon='fa fa-user-secret', permissions=[permission_role_view],
|
||||
text=_('Roles'), view='permissions:role_list'
|
||||
)
|
||||
link_role_members = Link(
|
||||
permissions=[permission_role_edit], text=_('Members'),
|
||||
view='permissions:role_members', args='object.id'
|
||||
)
|
||||
link_role_permissions = Link(
|
||||
permissions=[permission_permission_grant, permission_permission_revoke],
|
||||
text=_('Role permissions'), view='permissions:role_permissions',
|
||||
args='object.id'
|
||||
)
|
||||
|
||||
@@ -12,6 +12,9 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
for permission in StoredPermission.objects.all():
|
||||
try:
|
||||
Permission.get({'pk': '%s.%s' % (permission.namespace, permission.name)}, proxy_only=True)
|
||||
Permission.get(
|
||||
{'pk': '%s.%s' % (permission.namespace, permission.name)},
|
||||
proxy_only=True
|
||||
)
|
||||
except KeyError:
|
||||
permission.delete()
|
||||
|
||||
@@ -11,4 +11,6 @@ logger = logging.getLogger(__name__)
|
||||
class StoredPermissionManager(models.Manager):
|
||||
def get_for_holder(self, holder):
|
||||
ct = ContentType.objects.get_for_model(holder)
|
||||
return self.model.objects.filter(permissionholder__holder_type=ct).filter(permissionholder__holder_id=holder.pk)
|
||||
return self.model.objects.filter(
|
||||
permissionholder__holder_type=ct
|
||||
).filter(permissionholder__holder_id=holder.pk)
|
||||
|
||||
@@ -31,7 +31,10 @@ class StoredPermission(models.Model):
|
||||
|
||||
super(StoredPermission, self).__init__(*args, **kwargs)
|
||||
try:
|
||||
self.volatile_permission = Permission.get({'pk': '%s.%s' % (self.namespace, self.name)}, proxy_only=True)
|
||||
self.volatile_permission = Permission.get(
|
||||
{'pk': '%s.%s' % (self.namespace, self.name)},
|
||||
proxy_only=True
|
||||
)
|
||||
except KeyError:
|
||||
# Must be a deprecated permission in the database that is no
|
||||
# longer used in the current code
|
||||
@@ -57,9 +60,15 @@ class StoredPermission(models.Model):
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Role(models.Model):
|
||||
label = models.CharField(max_length=64, unique=True, verbose_name=_('Label'))
|
||||
permissions = models.ManyToManyField(StoredPermission, related_name='roles', verbose_name=_('Permissions'))
|
||||
groups = models.ManyToManyField(Group, related_name='roles', verbose_name=_('Groups'))
|
||||
label = models.CharField(
|
||||
max_length=64, unique=True, verbose_name=_('Label')
|
||||
)
|
||||
permissions = models.ManyToManyField(
|
||||
StoredPermission, related_name='roles', verbose_name=_('Permissions')
|
||||
)
|
||||
groups = models.ManyToManyField(
|
||||
Group, related_name='roles', verbose_name=_('Groups')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('label',)
|
||||
|
||||
@@ -6,9 +6,21 @@ from . import PermissionNamespace
|
||||
|
||||
namespace = PermissionNamespace('permissions', _('Permissions'))
|
||||
|
||||
permission_role_view = namespace.add_permission(name='role_view', label=_('View roles'))
|
||||
permission_role_edit = namespace.add_permission(name='role_edit', label=_('Edit roles'))
|
||||
permission_role_create = namespace.add_permission(name='role_create', label=_('Create roles'))
|
||||
permission_role_delete = namespace.add_permission(name='role_delete', label=_('Delete roles'))
|
||||
permission_permission_grant = namespace.add_permission(name='permission_grant', label=_('Grant permissions'))
|
||||
permission_permission_revoke = namespace.add_permission(name='permission_revoke', label=_('Revoke permissions'))
|
||||
permission_role_view = namespace.add_permission(
|
||||
name='role_view', label=_('View roles')
|
||||
)
|
||||
permission_role_edit = namespace.add_permission(
|
||||
name='role_edit', label=_('Edit roles')
|
||||
)
|
||||
permission_role_create = namespace.add_permission(
|
||||
name='role_create', label=_('Create roles')
|
||||
)
|
||||
permission_role_delete = namespace.add_permission(
|
||||
name='role_delete', label=_('Delete roles')
|
||||
)
|
||||
permission_permission_grant = namespace.add_permission(
|
||||
name='permission_grant', label=_('Grant permissions')
|
||||
)
|
||||
permission_permission_revoke = namespace.add_permission(
|
||||
name='permission_revoke', label=_('Revoke permissions')
|
||||
)
|
||||
|
||||
@@ -24,7 +24,9 @@ class PermissionTestCase(TestCase):
|
||||
|
||||
def test_no_permissions(self):
|
||||
with self.assertRaises(PermissionDenied):
|
||||
Permission.check_permissions(requester=self.user, permissions=(permission_role_view,))
|
||||
Permission.check_permissions(
|
||||
requester=self.user, permissions=(permission_role_view,)
|
||||
)
|
||||
|
||||
def test_with_permissions(self):
|
||||
self.group.user_set.add(self.user)
|
||||
@@ -32,6 +34,8 @@ class PermissionTestCase(TestCase):
|
||||
self.role.groups.add(self.group)
|
||||
|
||||
try:
|
||||
Permission.check_permissions(requester=self.user, permissions=(permission_role_view,))
|
||||
Permission.check_permissions(
|
||||
requester=self.user, permissions=(permission_role_view,)
|
||||
)
|
||||
except PermissionDenied:
|
||||
self.fail('PermissionDenied exception was not expected.')
|
||||
|
||||
@@ -12,10 +12,19 @@ urlpatterns = patterns(
|
||||
'permissions.views',
|
||||
url(r'^role/list/$', RoleListView.as_view(), name='role_list'),
|
||||
url(r'^role/create/$', RoleCreateView.as_view(), name='role_create'),
|
||||
url(r'^role/(?P<pk>\d+)/permissions/$', SetupRolePermissionsView.as_view(), name='role_permissions'),
|
||||
url(
|
||||
r'^role/(?P<pk>\d+)/permissions/$', SetupRolePermissionsView.as_view(),
|
||||
name='role_permissions'
|
||||
),
|
||||
url(r'^role/(?P<pk>\d+)/edit/$', RoleEditView.as_view(), name='role_edit'),
|
||||
url(r'^role/(?P<pk>\d+)/delete/$', RoleDeleteView.as_view(), name='role_delete'),
|
||||
url(r'^role/(?P<pk>\d+)/members/$', SetupRoleMembersView.as_view(), name='role_members'),
|
||||
url(
|
||||
r'^role/(?P<pk>\d+)/delete/$', RoleDeleteView.as_view(),
|
||||
name='role_delete'
|
||||
),
|
||||
url(
|
||||
r'^role/(?P<pk>\d+)/members/$', SetupRoleMembersView.as_view(),
|
||||
name='role_members'
|
||||
),
|
||||
)
|
||||
|
||||
api_urls = patterns(
|
||||
|
||||
@@ -54,10 +54,14 @@ class SetupRoleMembersView(AssignRemoveView):
|
||||
return get_object_or_404(Role, pk=self.kwargs['pk'])
|
||||
|
||||
def left_list(self):
|
||||
return [(unicode(group.pk), group.name) for group in set(Group.objects.all()) - set(self.get_object().groups.all())]
|
||||
return [
|
||||
(unicode(group.pk), group.name) for group in set(Group.objects.all()) - set(self.get_object().groups.all())
|
||||
]
|
||||
|
||||
def right_list(self):
|
||||
return [(unicode(group.pk), group.name) for group in self.get_object().groups.all()]
|
||||
return [
|
||||
(unicode(group.pk), group.name) for group in self.get_object().groups.all()
|
||||
]
|
||||
|
||||
def remove(self, item):
|
||||
group = get_object_or_404(Group, pk=item)
|
||||
@@ -80,7 +84,9 @@ class SetupRolePermissionsView(AssignRemoveView):
|
||||
view_permission = permission_role_view
|
||||
|
||||
def add(self, item):
|
||||
Permission.check_permissions(self.request.user, permissions=(permission_permission_grant,))
|
||||
Permission.check_permissions(
|
||||
self.request.user, permissions=(permission_permission_grant,)
|
||||
)
|
||||
permission = get_object_or_404(StoredPermission, pk=item)
|
||||
self.get_object().permissions.add(permission)
|
||||
|
||||
@@ -90,21 +96,31 @@ class SetupRolePermissionsView(AssignRemoveView):
|
||||
def left_list(self):
|
||||
results = []
|
||||
for namespace, permissions in itertools.groupby(StoredPermission.objects.exclude(id__in=self.get_object().permissions.values_list('pk', flat=True)), lambda entry: entry.namespace):
|
||||
permission_options = [(unicode(permission.pk), permission) for permission in permissions]
|
||||
results.append((PermissionNamespace.get(namespace), permission_options))
|
||||
permission_options = [
|
||||
(unicode(permission.pk), permission) for permission in permissions
|
||||
]
|
||||
results.append(
|
||||
(PermissionNamespace.get(namespace), permission_options)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def right_list(self):
|
||||
results = []
|
||||
for namespace, permissions in itertools.groupby(self.get_object().permissions.all(), lambda entry: entry.namespace):
|
||||
permission_options = [(unicode(permission.pk), permission) for permission in permissions]
|
||||
results.append((PermissionNamespace.get(namespace), permission_options))
|
||||
permission_options = [
|
||||
(unicode(permission.pk), permission) for permission in permissions
|
||||
]
|
||||
results.append(
|
||||
(PermissionNamespace.get(namespace), permission_options)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def remove(self, item):
|
||||
Permission.check_permissions(self.request.user, permissions=(permission_permission_revoke,))
|
||||
Permission.check_permissions(
|
||||
self.request.user, permissions=(permission_permission_revoke,)
|
||||
)
|
||||
permission = get_object_or_404(StoredPermission, pk=item)
|
||||
self.get_object().permissions.remove(permission)
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ class APIEndPoint(object):
|
||||
self.name = name
|
||||
self.endpoints = []
|
||||
try:
|
||||
api_urls = import_string('{0}.urls.api_urls'.format(app_name or name))
|
||||
api_urls = import_string(
|
||||
'{0}.urls.api_urls'.format(app_name or name)
|
||||
)
|
||||
except Exception:
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
|
||||
@@ -10,13 +10,17 @@ from permissions import Permission
|
||||
|
||||
class MayanObjectPermissionsFilter(BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
required_permission = getattr(view, 'mayan_object_permissions', {}).get(request.method, None)
|
||||
required_permission = getattr(
|
||||
view, 'mayan_object_permissions', {}).get(request.method, None
|
||||
)
|
||||
|
||||
if required_permission:
|
||||
try:
|
||||
Permission.check_permissions(request.user, required_permission)
|
||||
except PermissionDenied:
|
||||
return AccessControlList.objects.filter_by_access(required_permission[0], request.user, queryset)
|
||||
return AccessControlList.objects.filter_by_access(
|
||||
required_permission[0], request.user, queryset
|
||||
)
|
||||
else:
|
||||
return queryset
|
||||
else:
|
||||
|
||||
@@ -5,4 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from navigation import Link
|
||||
|
||||
link_api = Link(icon='fa fa-plug', text=_('REST API'), view='api-root')
|
||||
link_api_documentation = Link(icon='fa fa-book', text=_('API Documentation'), view='django.swagger.base.view')
|
||||
link_api_documentation = Link(
|
||||
icon='fa fa-book', text=_('API Documentation'),
|
||||
view='django.swagger.base.view'
|
||||
)
|
||||
|
||||
@@ -12,7 +12,9 @@ from permissions import Permission
|
||||
|
||||
class MayanPermission(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
required_permission = getattr(view, 'mayan_view_permissions', {}).get(request.method, None)
|
||||
required_permission = getattr(
|
||||
view, 'mayan_view_permissions', {}).get(request.method, None
|
||||
)
|
||||
|
||||
if required_permission:
|
||||
try:
|
||||
@@ -25,7 +27,9 @@ class MayanPermission(BasePermission):
|
||||
return True
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
required_permission = getattr(view, 'mayan_object_permissions', {}).get(request.method, None)
|
||||
required_permission = getattr(
|
||||
view, 'mayan_object_permissions', {}).get(request.method, None
|
||||
)
|
||||
|
||||
if required_permission:
|
||||
try:
|
||||
@@ -33,9 +37,15 @@ class MayanPermission(BasePermission):
|
||||
except PermissionDenied:
|
||||
try:
|
||||
if hasattr(view, 'mayan_permission_attribute_check'):
|
||||
AccessControlList.objects.check_access(required_permission, request.user, getattr(obj, view.mayan_permission_attribute_check))
|
||||
AccessControlList.objects.check_access(
|
||||
required_permission, request.user, getattr(
|
||||
obj, view.mayan_permission_attribute_check
|
||||
)
|
||||
)
|
||||
else:
|
||||
AccessControlList.objects.check_access(required_permission, request.user, obj)
|
||||
AccessControlList.objects.check_access(
|
||||
required_permission, request.user, obj
|
||||
)
|
||||
except PermissionDenied:
|
||||
return False
|
||||
else:
|
||||
|
||||
@@ -7,7 +7,9 @@ from .views import APIBase, Version_0, APIAppView, BrowseableObtainAuthToken
|
||||
version_0_urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^$', Version_0.as_view(), name='api-version-0'),
|
||||
url(r'^(?P<app_name>\w+)/$', APIAppView.as_view(), name='api-version-0-app'),
|
||||
url(
|
||||
r'^(?P<app_name>\w+)/$', APIAppView.as_view(), name='api-version-0-app'
|
||||
),
|
||||
)
|
||||
|
||||
urlpatterns = patterns(
|
||||
@@ -18,5 +20,8 @@ urlpatterns = patterns(
|
||||
|
||||
api_urls = patterns(
|
||||
'',
|
||||
url(r'^auth/token/obtain/', BrowseableObtainAuthToken.as_view(), name='auth_token_obtain'),
|
||||
url(
|
||||
r'^auth/token/obtain/', BrowseableObtainAuthToken.as_view(),
|
||||
name='auth_token_obtain'
|
||||
),
|
||||
)
|
||||
|
||||
@@ -25,7 +25,11 @@ class APIBase(generics.GenericAPIView):
|
||||
|
||||
def get(self, request, format=None):
|
||||
return Response([
|
||||
{'name': 'Version 0', 'url': reverse('api-version-0', request=request, format=format)}
|
||||
{
|
||||
'name': 'Version 0', 'url': reverse(
|
||||
'api-version-0', request=request, format=format
|
||||
)
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@@ -39,7 +43,11 @@ class Version_0(generics.GenericAPIView):
|
||||
def get(self, request, format=None):
|
||||
return Response({
|
||||
'apps': [
|
||||
{'name': unicode(endpoint), 'url': reverse('api-version-0-app', args=[unicode(endpoint)], request=request, format=format)} for endpoint in APIEndPoint.get_all()
|
||||
{
|
||||
'name': unicode(endpoint),
|
||||
'url': reverse('api-version-0-app',
|
||||
args=[unicode(endpoint)], request=request, format=format)
|
||||
} for endpoint in APIEndPoint.get_all()
|
||||
],
|
||||
})
|
||||
|
||||
@@ -54,10 +62,15 @@ class APIAppView(generics.GenericAPIView):
|
||||
def get(self, request, app_name, format=None):
|
||||
api_app = APIEndPoint.get(app_name)
|
||||
|
||||
return Response({
|
||||
'name': api_app.name,
|
||||
'url': reverse('api-version-0-app', args=[unicode(api_app.name)], request=request, format=format)
|
||||
})
|
||||
return Response(
|
||||
{
|
||||
'name': api_app.name,
|
||||
'url': reverse(
|
||||
'api-version-0-app', args=[unicode(api_app.name)],
|
||||
request=request, format=format
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class BrowseableObtainAuthToken(ObtainAuthToken):
|
||||
|
||||
@@ -27,12 +27,25 @@ class SmartSettingsApp(MayanAppConfig):
|
||||
def ready(self):
|
||||
super(SmartSettingsApp, self).ready()
|
||||
|
||||
SourceColumn(source=Namespace, label=_('Setting count'), attribute=encapsulate(lambda instance: len(instance.settings)))
|
||||
SourceColumn(source=Setting, label=_('Name'), attribute=encapsulate(lambda instance: setting_widget(instance)))
|
||||
SourceColumn(source=Setting, label=_('Value'), attribute='serialized_value')
|
||||
SourceColumn(source=Setting, label=_('Found in path'), attribute=encapsulate(lambda instance: exists_widget(instance.value) if instance.is_path else _('n/a')))
|
||||
SourceColumn(
|
||||
source=Namespace, label=_('Setting count'),
|
||||
attribute=encapsulate(lambda instance: len(instance.settings))
|
||||
)
|
||||
SourceColumn(
|
||||
source=Setting, label=_('Name'),
|
||||
attribute=encapsulate(lambda instance: setting_widget(instance))
|
||||
)
|
||||
SourceColumn(
|
||||
source=Setting, label=_('Value'), attribute='serialized_value'
|
||||
)
|
||||
SourceColumn(
|
||||
source=Setting, label=_('Found in path'),
|
||||
attribute=encapsulate(lambda instance: exists_widget(instance.value) if instance.is_path else _('n/a'))
|
||||
)
|
||||
|
||||
menu_object.bind_links(links=(link_namespace_detail,), sources=(Namespace,))
|
||||
menu_object.bind_links(
|
||||
links=(link_namespace_detail,), sources=(Namespace,)
|
||||
)
|
||||
menu_setup.bind_links(links=(link_namespace_list,))
|
||||
|
||||
for app in apps.get_app_configs():
|
||||
|
||||
@@ -23,7 +23,9 @@ class Namespace(object):
|
||||
|
||||
def __init__(self, name, label):
|
||||
if name in self.__class__._registry:
|
||||
raise Exception('Namespace names must be unique; "%s" already exists.' % name)
|
||||
raise Exception(
|
||||
'Namespace names must be unique; "%s" already exists.' % name
|
||||
)
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.__class__._registry[name] = self
|
||||
@@ -59,7 +61,9 @@ class Setting(object):
|
||||
@property
|
||||
def serialized_value(self):
|
||||
if not self.yaml:
|
||||
self.yaml = Setting.serialize_value(getattr(settings, self.global_name, self.default))
|
||||
self.yaml = Setting.serialize_value(
|
||||
getattr(settings, self.global_name, self.default)
|
||||
)
|
||||
|
||||
return self.yaml
|
||||
|
||||
|
||||
@@ -9,5 +9,11 @@ def is_superuser(context):
|
||||
return context['request'].user.is_staff or context['request'].user.is_superuser
|
||||
|
||||
|
||||
link_namespace_list = Link(condition=is_superuser, icon='fa fa-sliders', text=_('Settings'), view='settings:namespace_list')
|
||||
link_namespace_detail = Link(condition=is_superuser, text=_('Settings'), view='settings:namespace_detail', args='resolved_object.name')
|
||||
link_namespace_list = Link(
|
||||
condition=is_superuser, icon='fa fa-sliders', text=_('Settings'),
|
||||
view='settings:namespace_list'
|
||||
)
|
||||
link_namespace_detail = Link(
|
||||
condition=is_superuser, text=_('Settings'),
|
||||
view='settings:namespace_detail', args='resolved_object.name'
|
||||
)
|
||||
|
||||
@@ -6,6 +6,11 @@ from .views import NamespaceDetailView, NamespaceListView
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^namespace/all/$', NamespaceListView.as_view(), name='namespace_list'),
|
||||
url(r'^namespace/(?P<namespace_name>\w+)/$', NamespaceDetailView.as_view(), name='namespace_detail'),
|
||||
url(
|
||||
r'^namespace/all/$', NamespaceListView.as_view(), name='namespace_list'
|
||||
),
|
||||
url(
|
||||
r'^namespace/(?P<namespace_name>\w+)/$', NamespaceDetailView.as_view(),
|
||||
name='namespace_detail'
|
||||
),
|
||||
)
|
||||
|
||||
@@ -23,8 +23,15 @@ class APIStagingSourceFileView(generics.GenericAPIView):
|
||||
serializer_class = StagingFolderFileSerializer
|
||||
|
||||
def get(self, request, staging_folder_pk, encoded_filename):
|
||||
staging_folder = get_object_or_404(StagingFolderSource, pk=staging_folder_pk)
|
||||
return Response(StagingFolderFileSerializer(staging_folder.get_file(encoded_filename=encoded_filename), context={'request': request}).data)
|
||||
staging_folder = get_object_or_404(
|
||||
StagingFolderSource, pk=staging_folder_pk
|
||||
)
|
||||
return Response(
|
||||
StagingFolderFileSerializer(
|
||||
staging_folder.get_file(encoded_filename=encoded_filename),
|
||||
context={'request': request}
|
||||
).data
|
||||
)
|
||||
|
||||
|
||||
class APIStagingSourceListView(generics.ListAPIView):
|
||||
@@ -55,17 +62,36 @@ class APIStagingSourceFileImageView(generics.GenericAPIView):
|
||||
serializer_class = StagingSourceFileImageSerializer
|
||||
|
||||
def get(self, request, staging_folder_pk, encoded_filename):
|
||||
staging_folder = get_object_or_404(StagingFolderSource, pk=staging_folder_pk)
|
||||
staging_file = staging_folder.get_file(encoded_filename=encoded_filename)
|
||||
staging_folder = get_object_or_404(
|
||||
StagingFolderSource, pk=staging_folder_pk
|
||||
)
|
||||
staging_file = staging_folder.get_file(
|
||||
encoded_filename=encoded_filename
|
||||
)
|
||||
|
||||
size = request.GET.get('size', setting_display_size.value)
|
||||
|
||||
try:
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'data': staging_file.get_image(as_base64=True, size=size, transformations=Transformation.objects.get_for_model(staging_folder, as_classes=True))
|
||||
'data': staging_file.get_image(
|
||||
as_base64=True, size=size,
|
||||
transformations=Transformation.objects.get_for_model(
|
||||
staging_folder, as_classes=True
|
||||
)
|
||||
)
|
||||
})
|
||||
except UnknownFileFormat as exception:
|
||||
return Response({'status': 'error', 'detail': 'unknown_file_format', 'message': unicode(exception)})
|
||||
return Response(
|
||||
{
|
||||
'status': 'error', 'detail': 'unknown_file_format',
|
||||
'message': unicode(exception)
|
||||
}
|
||||
)
|
||||
except UnkownConvertError as exception:
|
||||
return Response({'status': 'error', 'detail': 'converter_error', 'message': unicode(exception)})
|
||||
return Response(
|
||||
{
|
||||
'status': 'error', 'detail': 'converter_error',
|
||||
'message': unicode(exception)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -47,14 +47,34 @@ class SourcesApp(MayanAppConfig):
|
||||
|
||||
APIEndPoint('sources')
|
||||
|
||||
MissingItem(label=_('Create a document source'), description=_('Document sources are the way in which new documents are feed to Mayan EDMS, create at least a web form source to be able to upload documents from a browser.'), condition=lambda: not Source.objects.exists(), view='sources:setup_source_list')
|
||||
MissingItem(
|
||||
label=_('Create a document source'),
|
||||
description=_('Document sources are the way in which new documents are feed to Mayan EDMS, create at least a web form source to be able to upload documents from a browser.'),
|
||||
condition=lambda: not Source.objects.exists(),
|
||||
view='sources:setup_source_list'
|
||||
)
|
||||
|
||||
SourceColumn(source=StagingFile, label=_('Thumbnail'), attribute=encapsulate(lambda staging_file: staging_file_thumbnail(staging_file, gallery_name='sources:staging_list', title=staging_file.filename, size='100')))
|
||||
SourceColumn(
|
||||
source=StagingFile,
|
||||
label=_('Thumbnail'),
|
||||
attribute=encapsulate(
|
||||
lambda staging_file: staging_file_thumbnail(
|
||||
staging_file,
|
||||
gallery_name='sources:staging_list',
|
||||
title=staging_file.filename, size='100'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
app.conf.CELERY_QUEUES.extend(
|
||||
(
|
||||
Queue('sources', Exchange('sources'), routing_key='sources'),
|
||||
Queue('sources_periodic', Exchange('sources_periodic'), routing_key='sources_periodic', delivery_mode=1),
|
||||
Queue(
|
||||
'sources', Exchange('sources'), routing_key='sources'
|
||||
),
|
||||
Queue(
|
||||
'sources_periodic', Exchange('sources_periodic'),
|
||||
routing_key='sources_periodic', delivery_mode=1
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -73,13 +93,51 @@ class SourcesApp(MayanAppConfig):
|
||||
)
|
||||
|
||||
menu_front_page.bind_links(links=[link_document_create_multiple])
|
||||
menu_object.bind_links(links=[link_document_create_siblings], sources=[Document])
|
||||
menu_object.bind_links(links=[link_setup_source_edit, link_setup_source_delete, link_transformation_list, link_setup_source_logs], sources=[POP3Email, IMAPEmail, StagingFolderSource, WatchFolderSource, WebFormSource])
|
||||
menu_object.bind_links(links=[link_staging_file_delete], sources=[StagingFile])
|
||||
menu_secondary.bind_links(links=[link_setup_sources, link_setup_source_create_webform, link_setup_source_create_staging_folder, link_setup_source_create_pop3_email, link_setup_source_create_imap_email, link_setup_source_create_watch_folder], sources=[POP3Email, IMAPEmail, StagingFolderSource, WatchFolderSource, WebFormSource, 'sources:setup_source_list', 'sources:setup_source_create'])
|
||||
menu_object.bind_links(
|
||||
links=[link_document_create_siblings], sources=[Document]
|
||||
)
|
||||
menu_object.bind_links(
|
||||
links=[
|
||||
link_setup_source_edit, link_setup_source_delete,
|
||||
link_transformation_list, link_setup_source_logs
|
||||
], sources=[
|
||||
POP3Email, IMAPEmail, StagingFolderSource, WatchFolderSource,
|
||||
WebFormSource
|
||||
]
|
||||
)
|
||||
menu_object.bind_links(
|
||||
links=[link_staging_file_delete], sources=[StagingFile]
|
||||
)
|
||||
menu_secondary.bind_links(
|
||||
links=[
|
||||
link_setup_sources, link_setup_source_create_webform,
|
||||
link_setup_source_create_staging_folder,
|
||||
link_setup_source_create_pop3_email,
|
||||
link_setup_source_create_imap_email,
|
||||
link_setup_source_create_watch_folder
|
||||
], sources=[
|
||||
POP3Email, IMAPEmail, StagingFolderSource, WatchFolderSource,
|
||||
WebFormSource, 'sources:setup_source_list',
|
||||
'sources:setup_source_create'
|
||||
]
|
||||
)
|
||||
menu_setup.bind_links(links=[link_setup_sources])
|
||||
menu_sidebar.bind_links(links=[link_upload_version], sources=['documents:document_version_list', 'documents:upload_version', 'documents:document_version_revert'])
|
||||
menu_sidebar.bind_links(
|
||||
links=[link_upload_version],
|
||||
sources=[
|
||||
'documents:document_version_list', 'documents:upload_version',
|
||||
'documents:document_version_revert'
|
||||
]
|
||||
)
|
||||
|
||||
post_upgrade.connect(initialize_periodic_tasks, dispatch_uid='initialize_periodic_tasks')
|
||||
post_initial_setup.connect(create_default_document_source, dispatch_uid='create_default_document_source')
|
||||
post_version_upload.connect(copy_transformations_to_version, dispatch_uid='copy_transformations_to_version')
|
||||
post_upgrade.connect(
|
||||
initialize_periodic_tasks, dispatch_uid='initialize_periodic_tasks'
|
||||
)
|
||||
post_initial_setup.connect(
|
||||
create_default_document_source,
|
||||
dispatch_uid='create_default_document_source'
|
||||
)
|
||||
post_version_upload.connect(
|
||||
copy_transformations_to_version,
|
||||
dispatch_uid='copy_transformations_to_version'
|
||||
)
|
||||
|
||||
@@ -33,7 +33,9 @@ class SourceUploadedFile(File):
|
||||
class Attachment(File):
|
||||
def __init__(self, part, name):
|
||||
self.name = name
|
||||
self.file = PseudoFile(StringIO(part.get_payload(decode=True)), name=name)
|
||||
self.file = PseudoFile(
|
||||
StringIO(part.get_payload(decode=True)), name=name
|
||||
)
|
||||
|
||||
|
||||
class StagingFile(object):
|
||||
@@ -45,7 +47,9 @@ class StagingFile(object):
|
||||
self.staging_folder = staging_folder
|
||||
if encoded_filename:
|
||||
self.encoded_filename = str(encoded_filename)
|
||||
self.filename = base64.urlsafe_b64decode(urllib.unquote_plus(self.encoded_filename))
|
||||
self.filename = base64.urlsafe_b64decode(
|
||||
urllib.unquote_plus(self.encoded_filename)
|
||||
)
|
||||
else:
|
||||
self.filename = filename
|
||||
self.encoded_filename = base64.urlsafe_b64encode(filename)
|
||||
@@ -54,7 +58,9 @@ class StagingFile(object):
|
||||
return unicode(self.filename)
|
||||
|
||||
def as_file(self):
|
||||
return File(file=open(self.get_full_path(), mode='rb'), name=self.filename)
|
||||
return File(
|
||||
file=open(self.get_full_path(), mode='rb'), name=self.filename
|
||||
)
|
||||
|
||||
def get_full_path(self):
|
||||
return os.path.join(self.staging_folder.folder_path, self.filename)
|
||||
@@ -63,7 +69,11 @@ class StagingFile(object):
|
||||
converter = converter_class(file_object=open(self.get_full_path()))
|
||||
|
||||
if size:
|
||||
converter.transform(transformation=TransformationResize(**dict(zip(('width', 'height'), (size.split('x'))))))
|
||||
converter.transform(
|
||||
transformation=TransformationResize(
|
||||
**dict(zip(('width', 'height'), (size.split('x'))))
|
||||
)
|
||||
)
|
||||
|
||||
# Interactive transformations
|
||||
for transformation in transformations:
|
||||
|
||||
@@ -60,7 +60,6 @@ class StagingUploadForm(UploadBaseForm):
|
||||
]
|
||||
except Exception as exception:
|
||||
logger.error('exception: %s', exception)
|
||||
pass
|
||||
|
||||
staging_file_id = forms.ChoiceField(label=_('Staging file'))
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ from .models import POP3Email, IMAPEmail, WatchFolderSource, WebFormSource
|
||||
|
||||
|
||||
def create_default_document_source(sender, **kwargs):
|
||||
WebFormSource.objects.create(label=_('Default'), uncompress=SOURCE_UNCOMPRESS_CHOICE_ASK)
|
||||
WebFormSource.objects.create(
|
||||
label=_('Default'), uncompress=SOURCE_UNCOMPRESS_CHOICE_ASK
|
||||
)
|
||||
|
||||
|
||||
def copy_transformations_to_version(sender, **kwargs):
|
||||
@@ -17,7 +19,9 @@ def copy_transformations_to_version(sender, **kwargs):
|
||||
|
||||
# TODO: Fix this, source should be previous version
|
||||
# TODO: Fix this, shouldn't this be at the documents app
|
||||
Transformation.objects.copy(source=instance.document, targets=instance.pages.all())
|
||||
Transformation.objects.copy(
|
||||
source=instance.document, targets=instance.pages.all()
|
||||
)
|
||||
|
||||
|
||||
def initialize_periodic_tasks(**kwargs):
|
||||
|
||||
@@ -17,17 +17,68 @@ from .permissions import (
|
||||
)
|
||||
|
||||
|
||||
link_document_create_multiple = Link(icon='fa fa-upload', permissions=[permission_document_create], text=_('New document'), view='sources:document_create_multiple')
|
||||
link_document_create_siblings = Link(permissions=[permission_document_create], text=_('Clone'), view='sources:document_create_siblings', args='object.id')
|
||||
link_setup_sources = Link(icon='fa fa-upload', permissions=[permission_sources_setup_view], text=_('Sources'), view='sources:setup_source_list')
|
||||
link_setup_source_create_imap_email = Link(permissions=[permission_sources_setup_create], text=_('Add new imap email'), view='sources:setup_source_create', args='"%s"' % SOURCE_CHOICE_EMAIL_IMAP)
|
||||
link_setup_source_create_pop3_email = Link(permissions=[permission_sources_setup_create], text=_('Add new pop3 email'), view='sources:setup_source_create', args='"%s"' % SOURCE_CHOICE_EMAIL_POP3)
|
||||
link_setup_source_create_staging_folder = Link(permissions=[permission_sources_setup_create], text=_('Add new staging folder'), view='sources:setup_source_create', args='"%s"' % SOURCE_CHOICE_STAGING)
|
||||
link_setup_source_create_watch_folder = Link(permissions=[permission_sources_setup_create], text=_('Add new watch folder'), view='sources:setup_source_create', args='"%s"' % SOURCE_CHOICE_WATCH)
|
||||
link_setup_source_create_webform = Link(permissions=[permission_sources_setup_create], text=_('Add new webform source'), view='sources:setup_source_create', args='"%s"' % SOURCE_CHOICE_WEB_FORM)
|
||||
link_setup_source_delete = Link(permissions=[permission_sources_setup_delete], tags='dangerous', text=_('Delete'), view='sources:setup_source_delete', args=['resolved_object.pk'])
|
||||
link_setup_source_edit = Link(text=_('Edit'), view='sources:setup_source_edit', args=['resolved_object.pk'], permissions=[permission_sources_setup_edit])
|
||||
link_source_list = Link(permissions=[permission_sources_setup_view], text=_('Document sources'), view='sources:setup_web_form_list')
|
||||
link_staging_file_delete = Link(keep_query=True, permissions=[permission_document_new_version, permission_document_create], tags='dangerous', text=_('Delete'), view='sources:staging_file_delete', args=['source.pk', 'object.encoded_filename'])
|
||||
link_upload_version = Link(permissions=[permission_document_new_version], text=_('Upload new version'), view='sources:upload_version', args='object.pk')
|
||||
link_setup_source_logs = Link(text=_('Logs'), view='sources:setup_source_logs', args=['resolved_object.pk'], permissions=[permission_sources_setup_view])
|
||||
link_document_create_multiple = Link(
|
||||
icon='fa fa-upload', permissions=[permission_document_create],
|
||||
text=_('New document'), view='sources:document_create_multiple'
|
||||
)
|
||||
link_document_create_siblings = Link(
|
||||
permissions=[permission_document_create], text=_('Clone'),
|
||||
view='sources:document_create_siblings', args='object.id'
|
||||
)
|
||||
link_setup_sources = Link(
|
||||
icon='fa fa-upload', permissions=[permission_sources_setup_view],
|
||||
text=_('Sources'), view='sources:setup_source_list'
|
||||
)
|
||||
link_setup_source_create_imap_email = Link(
|
||||
permissions=[permission_sources_setup_create],
|
||||
text=_('Add new imap email'), view='sources:setup_source_create',
|
||||
args='"%s"' % SOURCE_CHOICE_EMAIL_IMAP
|
||||
)
|
||||
link_setup_source_create_pop3_email = Link(
|
||||
permissions=[permission_sources_setup_create],
|
||||
text=_('Add new pop3 email'), view='sources:setup_source_create',
|
||||
args='"%s"' % SOURCE_CHOICE_EMAIL_POP3
|
||||
)
|
||||
link_setup_source_create_staging_folder = Link(
|
||||
permissions=[permission_sources_setup_create],
|
||||
text=_('Add new staging folder'), view='sources:setup_source_create',
|
||||
args='"%s"' % SOURCE_CHOICE_STAGING
|
||||
)
|
||||
link_setup_source_create_watch_folder = Link(
|
||||
permissions=[permission_sources_setup_create],
|
||||
text=_('Add new watch folder'), view='sources:setup_source_create',
|
||||
args='"%s"' % SOURCE_CHOICE_WATCH
|
||||
)
|
||||
link_setup_source_create_webform = Link(
|
||||
permissions=[permission_sources_setup_create],
|
||||
text=_('Add new webform source'), view='sources:setup_source_create',
|
||||
args='"%s"' % SOURCE_CHOICE_WEB_FORM
|
||||
)
|
||||
link_setup_source_delete = Link(
|
||||
permissions=[permission_sources_setup_delete], tags='dangerous',
|
||||
text=_('Delete'), view='sources:setup_source_delete',
|
||||
args=['resolved_object.pk']
|
||||
)
|
||||
link_setup_source_edit = Link(
|
||||
text=_('Edit'), view='sources:setup_source_edit',
|
||||
args=['resolved_object.pk'], permissions=[permission_sources_setup_edit]
|
||||
)
|
||||
link_source_list = Link(
|
||||
permissions=[permission_sources_setup_view], text=_('Document sources'),
|
||||
view='sources:setup_web_form_list'
|
||||
)
|
||||
link_staging_file_delete = Link(
|
||||
keep_query=True,
|
||||
permissions=[permission_document_new_version, permission_document_create],
|
||||
tags='dangerous', text=_('Delete'), view='sources:staging_file_delete',
|
||||
args=['source.pk', 'object.encoded_filename']
|
||||
)
|
||||
link_upload_version = Link(
|
||||
permissions=[permission_document_new_version],
|
||||
text=_('Upload new version'), view='sources:upload_version',
|
||||
args='object.pk'
|
||||
)
|
||||
link_setup_source_logs = Link(
|
||||
text=_('Logs'), view='sources:setup_source_logs',
|
||||
args=['resolved_object.pk'], permissions=[permission_sources_setup_view]
|
||||
)
|
||||
|
||||
@@ -55,18 +55,31 @@ class Source(models.Model):
|
||||
def upload_document(self, file_object, document_type, description=None, label=None, language=None, metadata_dict_list=None, user=None):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
document = Document.objects.create(description=description or '', document_type=document_type, label=label or unicode(file_object), language=language or setting_language.value)
|
||||
document = Document.objects.create(
|
||||
description=description or '', document_type=document_type,
|
||||
label=label or unicode(file_object),
|
||||
language=language or setting_language.value
|
||||
)
|
||||
document.save(_user=user)
|
||||
|
||||
document_version = document.new_version(file_object=file_object, _user=user)
|
||||
document_version = document.new_version(
|
||||
file_object=file_object, _user=user
|
||||
)
|
||||
|
||||
Transformation.objects.copy(source=self, targets=document_version.pages.all())
|
||||
Transformation.objects.copy(
|
||||
source=self, targets=document_version.pages.all()
|
||||
)
|
||||
|
||||
if metadata_dict_list:
|
||||
save_metadata_list(metadata_dict_list, document, create=True)
|
||||
save_metadata_list(
|
||||
metadata_dict_list, document, create=True
|
||||
)
|
||||
|
||||
except Exception as exception:
|
||||
logger.critical('Unexpected exception while trying to create new document "%s" from source "%s"; %s', label or unicode(file_object), self, exception)
|
||||
logger.critical(
|
||||
'Unexpected exception while trying to create new document "%s" from source "%s"; %s',
|
||||
label or unicode(file_object), self, exception
|
||||
)
|
||||
raise
|
||||
|
||||
def handle_upload(self, file_object, description=None, document_type=None, expand=False, label=None, language=None, metadata_dict_list=None, user=None):
|
||||
@@ -84,7 +97,9 @@ class Source(models.Model):
|
||||
compressed_file = CompressedFile(file_object)
|
||||
for compressed_file_child in compressed_file.children():
|
||||
kwargs.update({'label': unicode(compressed_file_child)})
|
||||
self.upload_document(file_object=File(compressed_file_child), **kwargs)
|
||||
self.upload_document(
|
||||
file_object=File(compressed_file_child), **kwargs
|
||||
)
|
||||
compressed_file_child.close()
|
||||
|
||||
except NotACompressedFile:
|
||||
@@ -119,11 +134,29 @@ class StagingFolderSource(InteractiveSource):
|
||||
is_interactive = True
|
||||
source_type = SOURCE_CHOICE_STAGING
|
||||
|
||||
folder_path = models.CharField(max_length=255, help_text=_('Server side filesystem path.'), verbose_name=_('Folder path'))
|
||||
preview_width = models.IntegerField(help_text=_('Width value to be passed to the converter backend.'), verbose_name=_('Preview width'))
|
||||
preview_height = models.IntegerField(blank=True, null=True, help_text=_('Height value to be passed to the converter backend.'), verbose_name=_('Preview height'))
|
||||
uncompress = models.CharField(choices=SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, max_length=1, help_text=_('Whether to expand or not compressed archives.'), verbose_name=_('Uncompress'))
|
||||
delete_after_upload = models.BooleanField(default=True, help_text=_('Delete the file after is has been successfully uploaded.'), verbose_name=_('Delete after upload'))
|
||||
folder_path = models.CharField(
|
||||
max_length=255, help_text=_('Server side filesystem path.'),
|
||||
verbose_name=_('Folder path')
|
||||
)
|
||||
preview_width = models.IntegerField(
|
||||
help_text=_('Width value to be passed to the converter backend.'),
|
||||
verbose_name=_('Preview width')
|
||||
)
|
||||
preview_height = models.IntegerField(
|
||||
blank=True, null=True,
|
||||
help_text=_('Height value to be passed to the converter backend.'),
|
||||
verbose_name=_('Preview height')
|
||||
)
|
||||
uncompress = models.CharField(
|
||||
choices=SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, max_length=1,
|
||||
help_text=_('Whether to expand or not compressed archives.'),
|
||||
verbose_name=_('Uncompress')
|
||||
)
|
||||
delete_after_upload = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_('Delete the file after is has been successfully uploaded.'),
|
||||
verbose_name=_('Delete after upload')
|
||||
)
|
||||
|
||||
def get_preview_size(self):
|
||||
dimensions = []
|
||||
@@ -141,20 +174,34 @@ class StagingFolderSource(InteractiveSource):
|
||||
for entry in sorted([os.path.normcase(f) for f in os.listdir(self.folder_path) if os.path.isfile(os.path.join(self.folder_path, f))]):
|
||||
yield self.get_file(filename=entry)
|
||||
except OSError as exception:
|
||||
logger.error('Unable get list of staging files from source: %s; %s', self, exception)
|
||||
raise Exception(_('Unable get list of staging files: %s') % exception)
|
||||
logger.error(
|
||||
'Unable get list of staging files from source: %s; %s', self,
|
||||
exception
|
||||
)
|
||||
raise Exception(
|
||||
_('Unable get list of staging files: %s') % exception
|
||||
)
|
||||
|
||||
def get_upload_file_object(self, form_data):
|
||||
staging_file = self.get_file(encoded_filename=form_data['staging_file_id'])
|
||||
return SourceUploadedFile(source=self, file=staging_file.as_file(), extra_data=staging_file)
|
||||
staging_file = self.get_file(
|
||||
encoded_filename=form_data['staging_file_id']
|
||||
)
|
||||
return SourceUploadedFile(
|
||||
source=self, file=staging_file.as_file(), extra_data=staging_file
|
||||
)
|
||||
|
||||
def clean_up_upload_file(self, upload_file_object):
|
||||
if self.delete_after_upload:
|
||||
try:
|
||||
upload_file_object.extra_data.delete()
|
||||
except Exception as exception:
|
||||
logger.error('Error deleting staging file: %s; %s', upload_file_object, exception)
|
||||
raise Exception(_('Error deleting staging file; %s') % exception)
|
||||
logger.error(
|
||||
'Error deleting staging file: %s; %s', upload_file_object,
|
||||
exception
|
||||
)
|
||||
raise Exception(
|
||||
_('Error deleting staging file; %s') % exception
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Staging folder')
|
||||
@@ -166,7 +213,11 @@ class WebFormSource(InteractiveSource):
|
||||
source_type = SOURCE_CHOICE_WEB_FORM
|
||||
|
||||
# TODO: unify uncompress as an InteractiveSource field
|
||||
uncompress = models.CharField(choices=SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, help_text=_('Whether to expand or not compressed archives.'), max_length=1, verbose_name=_('Uncompress'))
|
||||
uncompress = models.CharField(
|
||||
choices=SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES,
|
||||
help_text=_('Whether to expand or not compressed archives.'),
|
||||
max_length=1, verbose_name=_('Uncompress')
|
||||
)
|
||||
# Default path
|
||||
|
||||
def get_upload_file_object(self, form_data):
|
||||
@@ -186,16 +237,30 @@ class OutOfProcessSource(Source):
|
||||
|
||||
|
||||
class IntervalBaseModel(OutOfProcessSource):
|
||||
interval = models.PositiveIntegerField(default=DEFAULT_INTERVAL, help_text=_('Interval in seconds between checks for new documents.'), verbose_name=_('Interval'))
|
||||
document_type = models.ForeignKey(DocumentType, help_text=_('Assign a document type to documents uploaded from this source.'), verbose_name=_('Document type'))
|
||||
uncompress = models.CharField(choices=SOURCE_UNCOMPRESS_CHOICES, help_text=_('Whether to expand or not, compressed archives.'), max_length=1, verbose_name=_('Uncompress'))
|
||||
interval = models.PositiveIntegerField(
|
||||
default=DEFAULT_INTERVAL,
|
||||
help_text=_('Interval in seconds between checks for new documents.'),
|
||||
verbose_name=_('Interval')
|
||||
)
|
||||
document_type = models.ForeignKey(
|
||||
DocumentType,
|
||||
help_text=_('Assign a document type to documents uploaded from this source.'),
|
||||
verbose_name=_('Document type')
|
||||
)
|
||||
uncompress = models.CharField(
|
||||
choices=SOURCE_UNCOMPRESS_CHOICES,
|
||||
help_text=_('Whether to expand or not, compressed archives.'),
|
||||
max_length=1, verbose_name=_('Uncompress')
|
||||
)
|
||||
|
||||
def _get_periodic_task_name(self, pk=None):
|
||||
return 'check_interval_source-%i' % (pk or self.pk)
|
||||
|
||||
def _delete_periodic_task(self, pk=None):
|
||||
try:
|
||||
periodic_task = PeriodicTask.objects.get(name=self._get_periodic_task_name(pk))
|
||||
periodic_task = PeriodicTask.objects.get(
|
||||
name=self._get_periodic_task_name(pk)
|
||||
)
|
||||
|
||||
interval_instance = periodic_task.interval
|
||||
|
||||
@@ -205,7 +270,10 @@ class IntervalBaseModel(OutOfProcessSource):
|
||||
else:
|
||||
periodic_task.delete()
|
||||
except PeriodicTask.DoesNotExist:
|
||||
logger.warning('Tried to delete non existant periodic task "%s"', self._get_periodic_task_name(pk))
|
||||
logger.warning(
|
||||
'Tried to delete non existant periodic task "%s"',
|
||||
self._get_periodic_task_name(pk)
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
new_source = not self.pk
|
||||
@@ -214,7 +282,9 @@ class IntervalBaseModel(OutOfProcessSource):
|
||||
if not new_source:
|
||||
self._delete_periodic_task()
|
||||
|
||||
interval_instance, created = IntervalSchedule.objects.get_or_create(every=self.interval, period='seconds')
|
||||
interval_instance, created = IntervalSchedule.objects.get_or_create(
|
||||
every=self.interval, period='seconds'
|
||||
)
|
||||
# Create a new interval or reuse someone else's
|
||||
PeriodicTask.objects.create(
|
||||
name=self._get_periodic_task_name(),
|
||||
@@ -236,7 +306,10 @@ class IntervalBaseModel(OutOfProcessSource):
|
||||
class EmailBaseModel(IntervalBaseModel):
|
||||
host = models.CharField(max_length=128, verbose_name=_('Host'))
|
||||
ssl = models.BooleanField(default=True, verbose_name=_('SSL'))
|
||||
port = models.PositiveIntegerField(blank=True, null=True, help_text=_('Typical choices are 110 for POP3, 995 for POP3 over SSL, 143 for IMAP, 993 for IMAP over SSL.'), verbose_name=_('Port'))
|
||||
port = models.PositiveIntegerField(blank=True, null=True, help_text=_(
|
||||
'Typical choices are 110 for POP3, 995 for POP3 over SSL, 143 for IMAP, 993 for IMAP over SSL.'),
|
||||
verbose_name=_('Port')
|
||||
)
|
||||
username = models.CharField(max_length=96, verbose_name=_('Username'))
|
||||
password = models.CharField(max_length=96, verbose_name=_('Password'))
|
||||
|
||||
@@ -264,7 +337,11 @@ class EmailBaseModel(IntervalBaseModel):
|
||||
logger.debug('filename: %s', filename)
|
||||
|
||||
file_object = Attachment(part, name=filename)
|
||||
source.handle_upload(document_type=source.document_type, file_object=file_object, label=filename, expand=(source.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y))
|
||||
source.handle_upload(
|
||||
document_type=source.document_type,
|
||||
file_object=file_object, label=filename,
|
||||
expand=(source.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Email source')
|
||||
@@ -274,7 +351,9 @@ class EmailBaseModel(IntervalBaseModel):
|
||||
class POP3Email(EmailBaseModel):
|
||||
source_type = SOURCE_CHOICE_EMAIL_POP3
|
||||
|
||||
timeout = models.PositiveIntegerField(default=DEFAULT_POP3_TIMEOUT, verbose_name=_('Timeout'))
|
||||
timeout = models.PositiveIntegerField(
|
||||
default=DEFAULT_POP3_TIMEOUT, verbose_name=_('Timeout')
|
||||
)
|
||||
|
||||
def check_source(self):
|
||||
logger.debug('Starting POP3 email fetch')
|
||||
@@ -302,7 +381,9 @@ class POP3Email(EmailBaseModel):
|
||||
|
||||
complete_message = '\n'.join(mailbox.retr(message_number)[1])
|
||||
|
||||
EmailBaseModel.process_message(source=self, message=complete_message)
|
||||
EmailBaseModel.process_message(
|
||||
source=self, message=complete_message
|
||||
)
|
||||
mailbox.dele(message_number)
|
||||
|
||||
mailbox.quit()
|
||||
@@ -315,7 +396,11 @@ class POP3Email(EmailBaseModel):
|
||||
class IMAPEmail(EmailBaseModel):
|
||||
source_type = SOURCE_CHOICE_EMAIL_IMAP
|
||||
|
||||
mailbox = models.CharField(default=DEFAULT_IMAP_MAILBOX, help_text=_('Mail from which to check for messages with attached documents.'), max_length=64, verbose_name=_('Mailbox'))
|
||||
mailbox = models.CharField(
|
||||
default=DEFAULT_IMAP_MAILBOX,
|
||||
help_text=_('Mail from which to check for messages with attached documents.'),
|
||||
max_length=64, verbose_name=_('Mailbox')
|
||||
)
|
||||
|
||||
# http://www.doughellmann.com/PyMOTW/imaplib/
|
||||
def check_source(self):
|
||||
@@ -354,7 +439,10 @@ class IMAPEmail(EmailBaseModel):
|
||||
class WatchFolderSource(IntervalBaseModel):
|
||||
source_type = SOURCE_CHOICE_WATCH
|
||||
|
||||
folder_path = models.CharField(help_text=_('Server side filesystem path.'), max_length=255, verbose_name=_('Folder path'))
|
||||
folder_path = models.CharField(
|
||||
help_text=_('Server side filesystem path.'), max_length=255,
|
||||
verbose_name=_('Folder path')
|
||||
)
|
||||
|
||||
def check_source(self):
|
||||
# Force self.folder_path to unicode to avoid os.listdir returning
|
||||
@@ -363,7 +451,11 @@ class WatchFolderSource(IntervalBaseModel):
|
||||
full_path = os.path.join(self.folder_path, file_name)
|
||||
if os.path.isfile(full_path):
|
||||
with File(file=open(full_path, mode='rb')) as file_object:
|
||||
self.handle_upload(file_object=file_object, expand=(self.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y), label=file_name)
|
||||
self.handle_upload(
|
||||
file_object=file_object,
|
||||
expand=(self.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y),
|
||||
label=file_name
|
||||
)
|
||||
os.unlink(full_path)
|
||||
|
||||
class Meta:
|
||||
@@ -372,9 +464,15 @@ class WatchFolderSource(IntervalBaseModel):
|
||||
|
||||
|
||||
class SourceLog(models.Model):
|
||||
source = models.ForeignKey(Source, related_name='logs', verbose_name=_('Source'))
|
||||
datetime = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('Date time'))
|
||||
message = models.TextField(blank=True, editable=False, verbose_name=_('Message'))
|
||||
source = models.ForeignKey(
|
||||
Source, related_name='logs', verbose_name=_('Source')
|
||||
)
|
||||
datetime = models.DateTimeField(
|
||||
auto_now_add=True, editable=False, verbose_name=_('Date time')
|
||||
)
|
||||
message = models.TextField(
|
||||
blank=True, editable=False, verbose_name=_('Message')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Log entry')
|
||||
|
||||
@@ -5,7 +5,15 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from permissions import PermissionNamespace
|
||||
|
||||
namespace = PermissionNamespace('sources_setup', _('Sources setup'))
|
||||
permission_sources_setup_create = namespace.add_permission(name='sources_setup_create', label=_('Create new document sources'))
|
||||
permission_sources_setup_delete = namespace.add_permission(name='sources_setup_delete', label=_('Delete document sources'))
|
||||
permission_sources_setup_edit = namespace.add_permission(name='sources_setup_edit', label=_('Edit document sources'))
|
||||
permission_sources_setup_view = namespace.add_permission(name='sources_setup_view', label=_('View existing document sources'))
|
||||
permission_sources_setup_create = namespace.add_permission(
|
||||
name='sources_setup_create', label=_('Create new document sources')
|
||||
)
|
||||
permission_sources_setup_delete = namespace.add_permission(
|
||||
name='sources_setup_delete', label=_('Delete document sources')
|
||||
)
|
||||
permission_sources_setup_edit = namespace.add_permission(
|
||||
name='sources_setup_edit', label=_('Edit document sources')
|
||||
)
|
||||
permission_sources_setup_view = namespace.add_permission(
|
||||
name='sources_setup_view', label=_('View existing document sources')
|
||||
)
|
||||
|
||||
@@ -16,10 +16,18 @@ class StagingFolderFileSerializer(serializers.Serializer):
|
||||
filename = serializers.CharField(max_length=255)
|
||||
|
||||
def get_url(self, obj):
|
||||
return reverse('stagingfolderfile-detail', args=[obj.staging_folder.pk, obj.encoded_filename], request=self.context.get('request'))
|
||||
return reverse(
|
||||
'stagingfolderfile-detail',
|
||||
args=[obj.staging_folder.pk, obj.encoded_filename],
|
||||
request=self.context.get('request')
|
||||
)
|
||||
|
||||
def get_image_url(self, obj):
|
||||
return reverse('stagingfolderfile-image-view', args=[obj.staging_folder.pk, obj.encoded_filename], request=self.context.get('request'))
|
||||
return reverse(
|
||||
'stagingfolderfile-image-view',
|
||||
args=[obj.staging_folder.pk, obj.encoded_filename],
|
||||
request=self.context.get('request')
|
||||
)
|
||||
|
||||
|
||||
class StagingFolderSerializer(serializers.HyperlinkedModelSerializer):
|
||||
@@ -27,7 +35,9 @@ class StagingFolderSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
def get_files(self, obj):
|
||||
try:
|
||||
return [StagingFolderFileSerializer(entry, context=self.context).data for entry in obj.get_files()]
|
||||
return [
|
||||
StagingFolderFileSerializer(entry, context=self.context).data for entry in obj.get_files()
|
||||
]
|
||||
except Exception as exception:
|
||||
logger.error('unhandled exception: %s', exception)
|
||||
return []
|
||||
|
||||
@@ -25,7 +25,9 @@ def task_check_interval_source(source_id):
|
||||
source.check_source()
|
||||
except Exception as exception:
|
||||
logger.error('Error processing source: %s; %s', source, exception)
|
||||
source.logs.create(message=_('Error processing source: %s') % exception)
|
||||
source.logs.create(
|
||||
message=_('Error processing source: %s') % exception
|
||||
)
|
||||
else:
|
||||
source.logs.all().delete()
|
||||
|
||||
@@ -43,29 +45,44 @@ def task_upload_document(self, source_id, document_type_id, shared_uploaded_file
|
||||
user = None
|
||||
|
||||
with shared_upload.open() as file_object:
|
||||
source.upload_document(file_object=file_object, document_type=document_type, description=description, label=label, language=language, metadata_dict_list=metadata_dict_list, user=user)
|
||||
source.upload_document(
|
||||
file_object=file_object, document_type=document_type,
|
||||
description=description, label=label, language=language,
|
||||
metadata_dict_list=metadata_dict_list, user=user
|
||||
)
|
||||
|
||||
except OperationalError as exception:
|
||||
logger.warning('Operational exception while trying to create new document "%s" from source id %d; %s. Retying.', label or shared_upload.filename, source_id, exception)
|
||||
logger.warning(
|
||||
'Operational exception while trying to create new document "%s" from source id %d; %s. Retying.',
|
||||
label or shared_upload.filename, source_id, exception
|
||||
)
|
||||
raise self.retry(exc=exception)
|
||||
else:
|
||||
try:
|
||||
shared_upload.delete()
|
||||
except OperationalError as exception:
|
||||
logger.warning('Operational error during attempt to delete shared upload file: %s; %s. Retrying.', shared_upload, exception)
|
||||
logger.warning(
|
||||
'Operational error during attempt to delete shared upload file: %s; %s. Retrying.',
|
||||
shared_upload, exception
|
||||
)
|
||||
|
||||
|
||||
@app.task(bind=True, default_retry_delay=DEFAULT_SOURCE_TASK_RETRY_DELAY, ignore_result=True)
|
||||
def task_source_handle_upload(self, document_type_id, shared_uploaded_file_id, source_id, description=None, expand=False, label=None, language=None, metadata_dict_list=None, skip_list=None, user_id=None):
|
||||
try:
|
||||
document_type = DocumentType.objects.get(pk=document_type_id)
|
||||
shared_upload = SharedUploadedFile.objects.get(pk=shared_uploaded_file_id)
|
||||
shared_upload = SharedUploadedFile.objects.get(
|
||||
pk=shared_uploaded_file_id
|
||||
)
|
||||
|
||||
if not label:
|
||||
label = shared_upload.filename
|
||||
|
||||
except OperationalError as exception:
|
||||
logger.warning('Operational error during attempt to load data to handle source upload: %s. Retrying.', exception)
|
||||
logger.warning(
|
||||
'Operational error during attempt to load data to handle source upload: %s. Retrying.',
|
||||
exception
|
||||
)
|
||||
raise self.retry(exc=exception)
|
||||
|
||||
kwargs = {
|
||||
@@ -89,9 +106,14 @@ def task_source_handle_upload(self, document_type_id, shared_uploaded_file_id, s
|
||||
kwargs.update({'label': unicode(compressed_file_child)})
|
||||
|
||||
try:
|
||||
child_shared_uploaded_file = SharedUploadedFile.objects.create(file=File(compressed_file_child))
|
||||
child_shared_uploaded_file = SharedUploadedFile.objects.create(
|
||||
file=File(compressed_file_child)
|
||||
)
|
||||
except OperationalError as exception:
|
||||
logger.warning('Operational error while preparing to upload child document: %s. Rescheduling.', exception)
|
||||
logger.warning(
|
||||
'Operational error while preparing to upload child document: %s. Rescheduling.',
|
||||
exception
|
||||
)
|
||||
|
||||
task_source_handle_upload.delay(
|
||||
document_type_id=document_type_id,
|
||||
@@ -105,7 +127,10 @@ def task_source_handle_upload(self, document_type_id, shared_uploaded_file_id, s
|
||||
return
|
||||
else:
|
||||
skip_list.append(unicode(compressed_file_child))
|
||||
task_upload_document.delay(shared_uploaded_file_id=child_shared_uploaded_file.pk, **kwargs)
|
||||
task_upload_document.delay(
|
||||
shared_uploaded_file_id=child_shared_uploaded_file.pk,
|
||||
**kwargs
|
||||
)
|
||||
finally:
|
||||
compressed_file_child.close()
|
||||
|
||||
@@ -113,9 +138,16 @@ def task_source_handle_upload(self, document_type_id, shared_uploaded_file_id, s
|
||||
try:
|
||||
shared_upload.delete()
|
||||
except OperationalError as exception:
|
||||
logger.warning('Operational error during attempt to delete shared upload file: %s; %s. Retrying.', shared_upload, exception)
|
||||
logger.warning(
|
||||
'Operational error during attempt to delete shared upload file: %s; %s. Retrying.',
|
||||
shared_upload, exception
|
||||
)
|
||||
except NotACompressedFile:
|
||||
logging.debug('Exception: NotACompressedFile')
|
||||
task_upload_document.delay(shared_uploaded_file_id=shared_upload.pk, **kwargs)
|
||||
task_upload_document.delay(
|
||||
shared_uploaded_file_id=shared_upload.pk, **kwargs
|
||||
)
|
||||
else:
|
||||
task_upload_document.delay(shared_uploaded_file_id=shared_upload.pk, **kwargs)
|
||||
task_upload_document.delay(
|
||||
shared_uploaded_file_id=shared_upload.pk, **kwargs
|
||||
)
|
||||
|
||||
@@ -27,12 +27,17 @@ class UploadDocumentTestCase(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
|
||||
self.document_type = DocumentType.objects.create(
|
||||
label=TEST_DOCUMENT_TYPE
|
||||
)
|
||||
ocr_settings = self.document_type.ocr_settings
|
||||
ocr_settings.auto_ocr = False
|
||||
ocr_settings.save()
|
||||
|
||||
self.admin_user = User.objects.create_superuser(username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD)
|
||||
self.admin_user = User.objects.create_superuser(
|
||||
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
|
||||
password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
self.client = Client()
|
||||
|
||||
def tearDown(self):
|
||||
@@ -41,14 +46,17 @@ class UploadDocumentTestCase(TestCase):
|
||||
|
||||
def test_issue_gh_163(self):
|
||||
"""
|
||||
Non-ASCII chars in document name failing in upload via watch folder #163
|
||||
https://github.com/mayan-edms/mayan-edms/issues/163
|
||||
Non-ASCII chars in document name failing in upload via watch folder
|
||||
gh-issue #163 https://github.com/mayan-edms/mayan-edms/issues/163
|
||||
"""
|
||||
|
||||
temporary_directory = tempfile.mkdtemp()
|
||||
shutil.copy(TEST_NON_ASCII_DOCUMENT_PATH, temporary_directory)
|
||||
|
||||
watch_folder = WatchFolderSource.objects.create(document_type=self.document_type, folder_path=temporary_directory, uncompress=SOURCE_UNCOMPRESS_CHOICE_Y)
|
||||
watch_folder = WatchFolderSource.objects.create(
|
||||
document_type=self.document_type, folder_path=temporary_directory,
|
||||
uncompress=SOURCE_UNCOMPRESS_CHOICE_Y
|
||||
)
|
||||
watch_folder.check_source()
|
||||
|
||||
self.assertEqual(Document.objects.count(), 1)
|
||||
@@ -65,7 +73,9 @@ class UploadDocumentTestCase(TestCase):
|
||||
|
||||
# Test Non-ASCII named documents inside Non-ASCII named compressed file
|
||||
|
||||
shutil.copy(TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH, temporary_directory)
|
||||
shutil.copy(
|
||||
TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH, temporary_directory
|
||||
)
|
||||
|
||||
watch_folder.check_source()
|
||||
document = Document.objects.all()[1]
|
||||
@@ -85,7 +95,9 @@ class UploadDocumentTestCase(TestCase):
|
||||
|
||||
class CompressedUploadsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
|
||||
self.document_type = DocumentType.objects.create(
|
||||
label=TEST_DOCUMENT_TYPE
|
||||
)
|
||||
|
||||
ocr_settings = self.document_type.ocr_settings
|
||||
ocr_settings.auto_ocr = False
|
||||
@@ -95,12 +107,25 @@ class CompressedUploadsTestCase(TestCase):
|
||||
self.document_type.delete()
|
||||
|
||||
def test_upload_compressed_file(self):
|
||||
source = WebFormSource(label='test source', uncompress=SOURCE_UNCOMPRESS_CHOICE_Y)
|
||||
source = WebFormSource(
|
||||
label='test source', uncompress=SOURCE_UNCOMPRESS_CHOICE_Y
|
||||
)
|
||||
|
||||
with open(TEST_COMPRESSED_DOCUMENT_PATH) as file_object:
|
||||
#self.document = self.document_type.new_document(file_object=File(file_object), label='small document')
|
||||
source.handle_upload(document_type=self.document_type, file_object=File(file_object), expand=(source.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y))
|
||||
source.handle_upload(
|
||||
document_type=self.document_type,
|
||||
file_object=File(file_object),
|
||||
expand=(source.uncompress == SOURCE_UNCOMPRESS_CHOICE_Y)
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 2)
|
||||
self.assertTrue('first document.pdf' in Document.objects.values_list('label', flat=True))
|
||||
self.assertTrue('second document.pdf' in Document.objects.values_list('label', flat=True))
|
||||
self.assertTrue(
|
||||
'first document.pdf' in Document.objects.values_list(
|
||||
'label', flat=True
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
'second document.pdf' in Document.objects.values_list(
|
||||
'label', flat=True
|
||||
)
|
||||
)
|
||||
|
||||
@@ -22,12 +22,17 @@ class UploadDocumentTestCase(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
|
||||
self.document_type = DocumentType.objects.create(
|
||||
label=TEST_DOCUMENT_TYPE
|
||||
)
|
||||
ocr_settings = self.document_type.ocr_settings
|
||||
ocr_settings.auto_ocr = False
|
||||
ocr_settings.save()
|
||||
|
||||
self.admin_user = User.objects.create_superuser(username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD)
|
||||
self.admin_user = User.objects.create_superuser(
|
||||
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
|
||||
password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
self.client = Client()
|
||||
|
||||
def tearDown(self):
|
||||
@@ -36,17 +41,30 @@ class UploadDocumentTestCase(TestCase):
|
||||
|
||||
def test_upload_a_document(self):
|
||||
# Login the admin user
|
||||
logged_in = self.client.login(username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD)
|
||||
logged_in = self.client.login(
|
||||
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
self.assertTrue(logged_in)
|
||||
self.assertTrue(self.admin_user.is_authenticated())
|
||||
|
||||
# Create new webform source
|
||||
self.client.post(reverse('sources:setup_source_create', args=[SOURCE_CHOICE_WEB_FORM]), {'label': 'test', 'uncompress': 'n', 'enabled': True})
|
||||
self.client.post(
|
||||
reverse(
|
||||
'sources:setup_source_create', args=[SOURCE_CHOICE_WEB_FORM]
|
||||
), {'label': 'test', 'uncompress': 'n', 'enabled': True}
|
||||
)
|
||||
self.assertEqual(WebFormSource.objects.count(), 1)
|
||||
|
||||
# Upload the test document
|
||||
with open(TEST_DOCUMENT_PATH) as file_descriptor:
|
||||
self.client.post(reverse('sources:upload_interactive'), {'document-language': 'eng', 'source-file': file_descriptor, 'document_type_id': self.document_type.pk, 'label': 'mayan_11_1.pdf'})
|
||||
self.client.post(
|
||||
reverse('sources:upload_interactive'),
|
||||
{
|
||||
'document-language': 'eng', 'source-file': file_descriptor,
|
||||
'document_type_id': self.document_type.pk,
|
||||
'label': 'mayan_11_1.pdf'
|
||||
}
|
||||
)
|
||||
self.assertEqual(Document.objects.count(), 1)
|
||||
|
||||
self.document = Document.objects.all().first()
|
||||
@@ -56,22 +74,37 @@ class UploadDocumentTestCase(TestCase):
|
||||
self.assertEqual(self.document.file_mimetype, 'application/pdf')
|
||||
self.assertEqual(self.document.file_mime_encoding, 'binary')
|
||||
self.assertEqual(self.document.label, 'mayan_11_1.pdf')
|
||||
self.assertEqual(self.document.checksum, 'c637ffab6b8bb026ed3784afdb07663fddc60099853fae2be93890852a69ecf3')
|
||||
self.assertEqual(
|
||||
self.document.checksum,
|
||||
'c637ffab6b8bb026ed3784afdb07663fddc60099853fae2be93890852a69ecf3'
|
||||
)
|
||||
self.assertEqual(self.document.page_count, 47)
|
||||
|
||||
def test_issue_25(self):
|
||||
# Login the admin user
|
||||
logged_in = self.client.login(username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD)
|
||||
logged_in = self.client.login(
|
||||
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
self.assertTrue(logged_in)
|
||||
self.assertTrue(self.admin_user.is_authenticated())
|
||||
|
||||
# Create new webform source
|
||||
self.client.post(reverse('sources:setup_source_create', args=[SOURCE_CHOICE_WEB_FORM]), {'label': 'test', 'uncompress': 'n', 'enabled': True})
|
||||
self.client.post(
|
||||
reverse(
|
||||
'sources:setup_source_create', args=[SOURCE_CHOICE_WEB_FORM]
|
||||
), {'label': 'test', 'uncompress': 'n', 'enabled': True}
|
||||
)
|
||||
self.assertEqual(WebFormSource.objects.count(), 1)
|
||||
|
||||
# Upload the test document
|
||||
with open(TEST_SMALL_DOCUMENT_PATH) as file_descriptor:
|
||||
self.client.post(reverse('sources:upload_interactive'), {'document-language': 'eng', 'source-file': file_descriptor, 'document_type_id': self.document_type.pk})
|
||||
self.client.post(
|
||||
reverse('sources:upload_interactive'),
|
||||
{
|
||||
'document-language': 'eng', 'source-file': file_descriptor,
|
||||
'document_type_id': self.document_type.pk
|
||||
}
|
||||
)
|
||||
self.assertEqual(Document.objects.count(), 1)
|
||||
|
||||
document = Document.objects.first()
|
||||
@@ -85,7 +118,13 @@ class UploadDocumentTestCase(TestCase):
|
||||
self.assertEqual(document.description, TEST_DOCUMENT_DESCRIPTION)
|
||||
|
||||
# Test for issue 25 during editing
|
||||
self.client.post(reverse('documents:document_edit', args=[document.pk]), {'description': TEST_DOCUMENT_DESCRIPTION, 'language': document.language, 'label': document.label})
|
||||
self.client.post(
|
||||
reverse('documents:document_edit', args=[document.pk]),
|
||||
{
|
||||
'description': TEST_DOCUMENT_DESCRIPTION,
|
||||
'language': document.language, 'label': document.label
|
||||
}
|
||||
)
|
||||
# Fetch document again and test description
|
||||
document = Document.objects.first()
|
||||
self.assertEqual(document.description, TEST_DOCUMENT_DESCRIPTION)
|
||||
|
||||
@@ -15,32 +15,81 @@ from .wizards import DocumentCreateWizard
|
||||
|
||||
urlpatterns = patterns(
|
||||
'sources.views',
|
||||
url(r'^staging_file/(?P<staging_folder_pk>\d+)/(?P<encoded_filename>.+)/delete/$', 'staging_file_delete', name='staging_file_delete'),
|
||||
url(
|
||||
r'^staging_file/(?P<staging_folder_pk>\d+)/(?P<encoded_filename>.+)/delete/$',
|
||||
'staging_file_delete', name='staging_file_delete'
|
||||
),
|
||||
|
||||
url(r'^upload/document/new/interactive/(?P<source_id>\d+)/$', UploadInteractiveView.as_view(), name='upload_interactive'),
|
||||
url(r'^upload/document/new/interactive/$', UploadInteractiveView.as_view(), name='upload_interactive'),
|
||||
url(
|
||||
r'^upload/document/new/interactive/(?P<source_id>\d+)/$',
|
||||
UploadInteractiveView.as_view(), name='upload_interactive'
|
||||
),
|
||||
url(
|
||||
r'^upload/document/new/interactive/$', UploadInteractiveView.as_view(),
|
||||
name='upload_interactive'
|
||||
),
|
||||
|
||||
url(r'^upload/document/(?P<document_pk>\d+)/version/interactive/(?P<source_id>\d+)/$', UploadInteractiveVersionView.as_view(), name='upload_version'),
|
||||
url(r'^upload/document/(?P<document_pk>\d+)/version/interactive/$', UploadInteractiveVersionView.as_view(), name='upload_version'),
|
||||
url(
|
||||
r'^upload/document/(?P<document_pk>\d+)/version/interactive/(?P<source_id>\d+)/$',
|
||||
UploadInteractiveVersionView.as_view(), name='upload_version'
|
||||
),
|
||||
url(
|
||||
r'^upload/document/(?P<document_pk>\d+)/version/interactive/$',
|
||||
UploadInteractiveVersionView.as_view(), name='upload_version'
|
||||
),
|
||||
|
||||
# Setup views
|
||||
|
||||
url(r'^setup/list/$', SetupSourceListView.as_view(), name='setup_source_list'),
|
||||
url(r'^setup/(?P<pk>\d+)/edit/$', SetupSourceEditView.as_view(), name='setup_source_edit'),
|
||||
url(r'^setup/(?P<pk>\d+)/logs/$', SourceLogListView.as_view(), name='setup_source_logs'),
|
||||
url(r'^setup/(?P<pk>\d+)/delete/$', SetupSourceDeleteView.as_view(), name='setup_source_delete'),
|
||||
url(r'^setup/(?P<source_type>\w+)/create/$', SetupSourceCreateView.as_view(), name='setup_source_create'),
|
||||
url(
|
||||
r'^setup/list/$', SetupSourceListView.as_view(),
|
||||
name='setup_source_list'
|
||||
),
|
||||
url(
|
||||
r'^setup/(?P<pk>\d+)/edit/$', SetupSourceEditView.as_view(),
|
||||
name='setup_source_edit'
|
||||
),
|
||||
url(
|
||||
r'^setup/(?P<pk>\d+)/logs/$', SourceLogListView.as_view(),
|
||||
name='setup_source_logs'
|
||||
),
|
||||
url(
|
||||
r'^setup/(?P<pk>\d+)/delete/$', SetupSourceDeleteView.as_view(),
|
||||
name='setup_source_delete'
|
||||
),
|
||||
url(
|
||||
r'^setup/(?P<source_type>\w+)/create/$',
|
||||
SetupSourceCreateView.as_view(), name='setup_source_create'
|
||||
),
|
||||
|
||||
# Document create views
|
||||
|
||||
url(r'^create/from/local/multiple/$', DocumentCreateWizard.as_view(), name='document_create_multiple'),
|
||||
url(r'^(?P<document_id>\d+)/create/siblings/$', 'document_create_siblings', name='document_create_siblings'),
|
||||
url(
|
||||
r'^create/from/local/multiple/$', DocumentCreateWizard.as_view(),
|
||||
name='document_create_multiple'
|
||||
),
|
||||
url(
|
||||
r'^(?P<document_id>\d+)/create/siblings/$', 'document_create_siblings',
|
||||
name='document_create_siblings'
|
||||
),
|
||||
)
|
||||
|
||||
api_urls = patterns(
|
||||
'',
|
||||
url(r'^staging_folders/file/(?P<staging_folder_pk>[0-9]+)/(?P<encoded_filename>.+)/image/$', APIStagingSourceFileImageView.as_view(), name='stagingfolderfile-image-view'),
|
||||
url(r'^staging_folders/file/(?P<staging_folder_pk>[0-9]+)/(?P<encoded_filename>.+)/$', APIStagingSourceFileView.as_view(), name='stagingfolderfile-detail'),
|
||||
url(r'^staging_folders/$', APIStagingSourceListView.as_view(), name='stagingfolder-list'),
|
||||
url(r'^staging_folders/(?P<pk>[0-9]+)/$', APIStagingSourceView.as_view(), name='stagingfolder-detail')
|
||||
url(
|
||||
r'^staging_folders/file/(?P<staging_folder_pk>[0-9]+)/(?P<encoded_filename>.+)/image/$',
|
||||
APIStagingSourceFileImageView.as_view(),
|
||||
name='stagingfolderfile-image-view'
|
||||
),
|
||||
url(
|
||||
r'^staging_folders/file/(?P<staging_folder_pk>[0-9]+)/(?P<encoded_filename>.+)/$',
|
||||
APIStagingSourceFileView.as_view(), name='stagingfolderfile-detail'
|
||||
),
|
||||
url(
|
||||
r'^staging_folders/$', APIStagingSourceListView.as_view(),
|
||||
name='stagingfolder-list'
|
||||
),
|
||||
url(
|
||||
r'^staging_folders/(?P<pk>[0-9]+)/$', APIStagingSourceView.as_view(),
|
||||
name='stagingfolder-detail'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -32,7 +32,8 @@ from .forms import (
|
||||
NewDocumentForm, NewVersionForm
|
||||
)
|
||||
from .literals import (
|
||||
SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WEB_FORM, SOURCE_UNCOMPRESS_CHOICE_ASK,
|
||||
SOURCE_CHOICE_STAGING, SOURCE_CHOICE_WEB_FORM,
|
||||
SOURCE_UNCOMPRESS_CHOICE_ASK,
|
||||
SOURCE_UNCOMPRESS_CHOICE_Y
|
||||
)
|
||||
from .models import (
|
||||
@@ -133,12 +134,18 @@ class UploadBaseView(MultiFormView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if 'source_id' in kwargs:
|
||||
self.source = get_object_or_404(Source.objects.filter(enabled=True).select_subclasses(), pk=kwargs['source_id'])
|
||||
self.source = get_object_or_404(
|
||||
Source.objects.filter(enabled=True).select_subclasses(),
|
||||
pk=kwargs['source_id']
|
||||
)
|
||||
else:
|
||||
self.source = InteractiveSource.objects.filter(enabled=True).select_subclasses().first()
|
||||
|
||||
if InteractiveSource.objects.filter(enabled=True).count() == 0:
|
||||
messages.error(request, _('No interactive document sources have been defined or none have been enabled, create one before proceeding.'))
|
||||
messages.error(
|
||||
request,
|
||||
_('No interactive document sources have been defined or none have been enabled, create one before proceeding.')
|
||||
)
|
||||
return HttpResponseRedirect(reverse('sources:setup_source_list'))
|
||||
|
||||
return super(UploadBaseView, self).dispatch(request, *args, **kwargs)
|
||||
@@ -198,9 +205,16 @@ class UploadInteractiveView(UploadBaseView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.subtemplates_list = []
|
||||
|
||||
Permission.check_permissions(request.user, [permission_document_create])
|
||||
Permission.check_permissions(
|
||||
request.user, [permission_document_create]
|
||||
)
|
||||
|
||||
self.document_type = get_object_or_404(DocumentType, pk=self.request.GET.get('document_type_id', self.request.POST.get('document_type_id')))
|
||||
self.document_type = get_object_or_404(
|
||||
DocumentType,
|
||||
pk=self.request.GET.get(
|
||||
'document_type_id', self.request.POST.get('document_type_id')
|
||||
)
|
||||
)
|
||||
|
||||
self.tab_links = get_active_tab_links()
|
||||
|
||||
@@ -215,9 +229,13 @@ class UploadInteractiveView(UploadBaseView):
|
||||
else:
|
||||
expand = False
|
||||
|
||||
uploaded_file = self.source.get_upload_file_object(forms['source_form'].cleaned_data)
|
||||
uploaded_file = self.source.get_upload_file_object(
|
||||
forms['source_form'].cleaned_data
|
||||
)
|
||||
|
||||
shared_uploaded_file = SharedUploadedFile.objects.create(file=uploaded_file.file)
|
||||
shared_uploaded_file = SharedUploadedFile.objects.create(
|
||||
file=uploaded_file.file
|
||||
)
|
||||
|
||||
if 'document_type_available_filenames' in forms['document_form'].cleaned_data:
|
||||
if forms['document_form'].cleaned_data['document_type_available_filenames']:
|
||||
@@ -246,14 +264,19 @@ class UploadInteractiveView(UploadBaseView):
|
||||
source_id=self.source.pk,
|
||||
user_id=user_id,
|
||||
))
|
||||
messages.success(self.request, _('New document queued for uploaded and will be available shortly.'))
|
||||
messages.success(
|
||||
self.request,
|
||||
_('New document queued for uploaded and will be available shortly.')
|
||||
)
|
||||
return HttpResponseRedirect(self.request.get_full_path())
|
||||
|
||||
def create_source_form_form(self, **kwargs):
|
||||
return self.get_form_classes()['source_form'](
|
||||
prefix=kwargs['prefix'],
|
||||
source=self.source,
|
||||
show_expand=(self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK),
|
||||
show_expand=(
|
||||
self.source.uncompress == SOURCE_UNCOMPRESS_CHOICE_ASK
|
||||
),
|
||||
data=kwargs.get('data', None),
|
||||
files=kwargs.get('files', None),
|
||||
)
|
||||
@@ -267,11 +290,16 @@ class UploadInteractiveView(UploadBaseView):
|
||||
)
|
||||
|
||||
def get_form_classes(self):
|
||||
return {'document_form': NewDocumentForm, 'source_form': get_upload_form_class(self.source.source_type)}
|
||||
return {
|
||||
'document_form': NewDocumentForm,
|
||||
'source_form': get_upload_form_class(self.source.source_type)
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UploadInteractiveView, self).get_context_data(**kwargs)
|
||||
context['title'] = _('Upload a local document from source: %s') % self.source.label
|
||||
context['title'] = _(
|
||||
'Upload a local document from source: %s'
|
||||
) % self.source.label
|
||||
return context
|
||||
|
||||
|
||||
@@ -282,18 +310,27 @@ class UploadInteractiveVersionView(UploadBaseView):
|
||||
|
||||
self.document = get_object_or_404(Document, pk=kwargs['document_pk'])
|
||||
try:
|
||||
Permission.check_permissions(self.request.user, [permission_document_new_version])
|
||||
Permission.check_permissions(
|
||||
self.request.user, [permission_document_new_version]
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(permission_document_new_version, self.request.user, self.document)
|
||||
AccessControlList.objects.check_access(
|
||||
permission_document_new_version, self.request.user,
|
||||
self.document
|
||||
)
|
||||
|
||||
self.tab_links = get_active_tab_links(self.document)
|
||||
|
||||
return super(UploadInteractiveVersionView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def forms_valid(self, forms):
|
||||
uploaded_file = self.source.get_upload_file_object(forms['source_form'].cleaned_data)
|
||||
uploaded_file = self.source.get_upload_file_object(
|
||||
forms['source_form'].cleaned_data
|
||||
)
|
||||
|
||||
shared_uploaded_file = SharedUploadedFile.objects.create(file=uploaded_file.file)
|
||||
shared_uploaded_file = SharedUploadedFile.objects.create(
|
||||
file=uploaded_file.file
|
||||
)
|
||||
|
||||
try:
|
||||
self.source.clean_up_upload_file(uploaded_file)
|
||||
@@ -312,8 +349,13 @@ class UploadInteractiveVersionView(UploadBaseView):
|
||||
comment=forms['document_form'].cleaned_data.get('comment')
|
||||
))
|
||||
|
||||
messages.success(self.request, _('New document version queued for uploaded and will be available shortly.'))
|
||||
return HttpResponseRedirect(reverse('documents:document_version_list', args=[self.document.pk]))
|
||||
messages.success(
|
||||
self.request,
|
||||
_('New document version queued for uploaded and will be available shortly.')
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse('documents:document_version_list', args=[self.document.pk])
|
||||
)
|
||||
|
||||
def create_source_form_form(self, **kwargs):
|
||||
return self.get_form_classes()['source_form'](
|
||||
@@ -332,30 +374,47 @@ class UploadInteractiveVersionView(UploadBaseView):
|
||||
)
|
||||
|
||||
def get_form_classes(self):
|
||||
return {'document_form': NewVersionForm, 'source_form': get_upload_form_class(self.source.source_type)}
|
||||
return {
|
||||
'document_form': NewVersionForm,
|
||||
'source_form': get_upload_form_class(self.source.source_type)
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UploadInteractiveVersionView, self).get_context_data(**kwargs)
|
||||
context['object'] = self.document
|
||||
context['title'] = _('Upload a new version from source: %s') % self.source.label
|
||||
context['title'] = _(
|
||||
'Upload a new version from source: %s'
|
||||
) % self.source.label
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def staging_file_delete(request, staging_folder_pk, encoded_filename):
|
||||
Permission.check_permissions(request.user, [permission_document_create, permission_document_new_version])
|
||||
staging_folder = get_object_or_404(StagingFolderSource, pk=staging_folder_pk)
|
||||
Permission.check_permissions(
|
||||
request.user, [
|
||||
permission_document_create, permission_document_new_version
|
||||
]
|
||||
)
|
||||
staging_folder = get_object_or_404(
|
||||
StagingFolderSource, pk=staging_folder_pk
|
||||
)
|
||||
|
||||
staging_file = staging_folder.get_file(encoded_filename=encoded_filename)
|
||||
next = request.POST.get('next', request.GET.get('next', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
next = request.POST.get(
|
||||
'next', request.GET.get('next', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))
|
||||
)
|
||||
previous = request.POST.get(
|
||||
'previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
staging_file.delete()
|
||||
messages.success(request, _('Staging file delete successfully.'))
|
||||
except Exception as exception:
|
||||
messages.error(request, _('Staging file delete error; %s.') % exception)
|
||||
messages.error(
|
||||
request, _('Staging file delete error; %s.') % exception
|
||||
)
|
||||
return HttpResponseRedirect(next)
|
||||
|
||||
results = get_active_tab_links()
|
||||
@@ -366,7 +425,11 @@ def staging_file_delete(request, staging_folder_pk, encoded_filename):
|
||||
'object': staging_file,
|
||||
'next': next,
|
||||
'previous': previous,
|
||||
'extra_navigation_links': {'form_header': {'staging_file_delete': {'links': results['tab_links']}}},
|
||||
'extra_navigation_links': {
|
||||
'form_header': {
|
||||
'staging_file_delete': {'links': results['tab_links']}
|
||||
}
|
||||
},
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
|
||||
@@ -390,7 +453,9 @@ class SetupSourceDeleteView(SingleObjectDeleteView):
|
||||
view_permission = permission_sources_setup_delete
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(Source.objects.select_subclasses(), pk=self.kwargs['pk'])
|
||||
return get_object_or_404(
|
||||
Source.objects.select_subclasses(), pk=self.kwargs['pk']
|
||||
)
|
||||
|
||||
def get_form_class(self):
|
||||
return get_form_class(self.get_object().source_type)
|
||||
@@ -407,7 +472,9 @@ class SetupSourceEditView(SingleObjectEditView):
|
||||
view_permission = permission_sources_setup_edit
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(Source.objects.select_subclasses(), pk=self.kwargs['pk'])
|
||||
return get_object_or_404(
|
||||
Source.objects.select_subclasses(), pk=self.kwargs['pk']
|
||||
)
|
||||
|
||||
def get_form_class(self):
|
||||
return get_form_class(self.get_object().source_type)
|
||||
@@ -431,7 +498,9 @@ class SetupSourceListView(SingleObjectListView):
|
||||
},
|
||||
{
|
||||
'name': _('Enabled'),
|
||||
'attribute': encapsulate(lambda entry: two_state_template(entry.enabled))
|
||||
'attribute': encapsulate(
|
||||
lambda entry: two_state_template(entry.enabled)
|
||||
)
|
||||
},
|
||||
],
|
||||
'hide_link': True,
|
||||
|
||||
@@ -14,7 +14,9 @@ from documents.settings import setting_preview_size, setting_thumbnail_size
|
||||
|
||||
|
||||
def staging_file_thumbnail(staging_file, **kwargs):
|
||||
return staging_file_html_widget(staging_file, click_view='stagingfolderfile-image-view', **kwargs)
|
||||
return staging_file_html_widget(
|
||||
staging_file, click_view='stagingfolderfile-image-view', **kwargs
|
||||
)
|
||||
|
||||
|
||||
def staging_file_html_widget(staging_file, click_view=None, page=DEFAULT_PAGE_NUMBER, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION, gallery_name=None, fancybox_class='fancybox-staging', image_class='lazy-load', title=None, size=setting_thumbnail_size.value, nolazyload=False):
|
||||
@@ -36,12 +38,25 @@ def staging_file_html_widget(staging_file, click_view=None, page=DEFAULT_PAGE_NU
|
||||
|
||||
query_string = urlencode(query_dict)
|
||||
|
||||
preview_view = '%s?%s' % (reverse('stagingfolderfile-image-view', args=[staging_file.staging_folder.pk, staging_file.encoded_filename]), query_string)
|
||||
preview_view = '%s?%s' % (
|
||||
reverse(
|
||||
'stagingfolderfile-image-view',
|
||||
args=[
|
||||
staging_file.staging_folder.pk, staging_file.encoded_filename
|
||||
]
|
||||
), query_string
|
||||
)
|
||||
|
||||
plain_template = []
|
||||
plain_template.append('<img src="%s" alt="%s" />' % (preview_view, alt_text))
|
||||
plain_template.append(
|
||||
'<img src="%s" alt="%s" />' % (preview_view, alt_text)
|
||||
)
|
||||
|
||||
result.append('<div class="tc" id="staging_file-%s-%d">' % (staging_file.filename, page if page else DEFAULT_PAGE_NUMBER))
|
||||
result.append(
|
||||
'<div class="tc" id="staging_file-%s-%d">' % (
|
||||
staging_file.filename, page if page else DEFAULT_PAGE_NUMBER
|
||||
)
|
||||
)
|
||||
|
||||
if title:
|
||||
title_template = 'title="%s"' % strip_tags(title)
|
||||
@@ -52,12 +67,35 @@ def staging_file_html_widget(staging_file, click_view=None, page=DEFAULT_PAGE_NU
|
||||
# TODO: fix this hack
|
||||
query_dict['size'] = setting_preview_size.value
|
||||
query_string = urlencode(query_dict)
|
||||
result.append('<a %s class="%s" href="%s" %s>' % (gallery_template, fancybox_class, '%s?%s' % (reverse(click_view, args=[staging_file.staging_folder.pk, staging_file.encoded_filename]), query_string), title_template))
|
||||
result.append(
|
||||
'<a %s class="%s" href="%s" %s>' % (
|
||||
gallery_template, fancybox_class,
|
||||
'%s?%s' % (
|
||||
reverse(
|
||||
click_view,
|
||||
args=[
|
||||
staging_file.staging_folder.pk,
|
||||
staging_file.encoded_filename
|
||||
]
|
||||
),
|
||||
query_string
|
||||
), title_template
|
||||
)
|
||||
)
|
||||
|
||||
if nolazyload:
|
||||
result.append('<img style="border: 1px solid black;" src="%s" alt="%s" />' % (preview_view, alt_text))
|
||||
result.append(
|
||||
'<img style="border: 1px solid black;" src="%s" alt="%s" />' % (
|
||||
preview_view, alt_text
|
||||
)
|
||||
)
|
||||
else:
|
||||
result.append('<img class="thin_border %s" data-src="%s" src="%s" alt="%s" />' % (image_class, preview_view, static('appearance/images/loading.png'), alt_text))
|
||||
result.append(
|
||||
'<img class="thin_border %s" data-src="%s" src="%s" alt="%s" />' % (
|
||||
image_class, preview_view,
|
||||
static('appearance/images/loading.png'), alt_text
|
||||
)
|
||||
)
|
||||
|
||||
if click_view:
|
||||
result.append('</a>')
|
||||
|
||||
@@ -31,7 +31,10 @@ class DocumentCreateWizard(ViewPermissionCheckMixin, SessionWizardView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if InteractiveSource.objects.filter(enabled=True).count() == 0:
|
||||
messages.error(request, _('No interactive document sources have been defined or none have been enabled, create one before proceeding.'))
|
||||
messages.error(
|
||||
request,
|
||||
_('No interactive document sources have been defined or none have been enabled, create one before proceeding.')
|
||||
)
|
||||
return HttpResponseRedirect(reverse('sources:setup_source_list'))
|
||||
return super(DocumentCreateWizard, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ class StatisticsApp(MayanAppConfig):
|
||||
super(StatisticsApp, self).ready()
|
||||
|
||||
menu_object.bind_links(links=[link_execute], sources=[Statistic])
|
||||
menu_object.bind_links(links=[link_namespace_details], sources=[StatisticNamespace])
|
||||
menu_secondary.bind_links(links=[link_namespace_list], sources=[StatisticNamespace, 'statistics:namespace_list'])
|
||||
menu_object.bind_links(
|
||||
links=[link_namespace_details], sources=[StatisticNamespace]
|
||||
)
|
||||
menu_secondary.bind_links(
|
||||
links=[link_namespace_list],
|
||||
sources=[StatisticNamespace, 'statistics:namespace_list']
|
||||
)
|
||||
menu_tools.bind_links(links=[link_statistics])
|
||||
|
||||
@@ -7,7 +7,19 @@ from navigation import Link
|
||||
from .permissions import permission_statistics_view
|
||||
|
||||
|
||||
link_execute = Link(permissions=[permission_statistics_view], text=_('Execute'), view='statistics:statistic_execute', args='resolved_object.id')
|
||||
link_namespace_details = Link(permissions=[permission_statistics_view], text=_('Namespace details'), view='statistics:namespace_details', args='resolved_object.id')
|
||||
link_namespace_list = Link(permissions=[permission_statistics_view], text=_('Namespace list'), view='statistics:namespace_list')
|
||||
link_statistics = Link(icon='fa fa-sort-numeric-desc', permissions=[permission_statistics_view], text=_('Statistics'), view='statistics:namespace_list')
|
||||
link_execute = Link(
|
||||
permissions=[permission_statistics_view], text=_('Execute'),
|
||||
view='statistics:statistic_execute', args='resolved_object.id'
|
||||
)
|
||||
link_namespace_details = Link(
|
||||
permissions=[permission_statistics_view], text=_('Namespace details'),
|
||||
view='statistics:namespace_details', args='resolved_object.id'
|
||||
)
|
||||
link_namespace_list = Link(
|
||||
permissions=[permission_statistics_view], text=_('Namespace list'),
|
||||
view='statistics:namespace_list'
|
||||
)
|
||||
link_statistics = Link(
|
||||
icon='fa fa-sort-numeric-desc', permissions=[permission_statistics_view],
|
||||
text=_('Statistics'), view='statistics:namespace_list'
|
||||
)
|
||||
|
||||
@@ -6,4 +6,6 @@ from permissions import PermissionNamespace
|
||||
|
||||
namespace = PermissionNamespace('statistics', _('Statistics'))
|
||||
|
||||
permission_statistics_view = namespace.add_permission(name='statistics_view', label=_('View statistics'))
|
||||
permission_statistics_view = namespace.add_permission(
|
||||
name='statistics_view', label=_('View statistics')
|
||||
)
|
||||
|
||||
@@ -7,6 +7,12 @@ from .views import NamespaceDetailView, NamespaceListView, StatisticExecute
|
||||
urlpatterns = patterns(
|
||||
'statistics.views',
|
||||
url(r'^$', NamespaceListView.as_view(), name='namespace_list'),
|
||||
url(r'^namespace/(?P<namespace_id>\w+)/details/$', NamespaceDetailView.as_view(), name='namespace_details'),
|
||||
url(r'^(?P<statistic_id>\w+)/execute/$', StatisticExecute.as_view(), name='statistic_execute'),
|
||||
url(
|
||||
r'^namespace/(?P<namespace_id>\w+)/details/$',
|
||||
NamespaceDetailView.as_view(), name='namespace_details'
|
||||
),
|
||||
url(
|
||||
r'^(?P<statistic_id>\w+)/execute/$', StatisticExecute.as_view(),
|
||||
name='statistic_execute'
|
||||
),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user