PEP8 cleanups.

This commit is contained in:
Roberto Rosario
2015-07-27 23:53:03 -04:00
parent 0b923a7a1c
commit 1e746c700a
101 changed files with 3244 additions and 892 deletions

View File

@@ -19,4 +19,6 @@ class ACLsApp(MayanAppConfig):
links=[link_acl_permissions, link_acl_delete],
sources=[AccessControlList]
)
menu_sidebar.bind_links(links=[link_acl_new], sources=['acls:acl_list'])
menu_sidebar.bind_links(
links=[link_acl_new], sources=['acls:acl_list']
)

View File

@@ -11,7 +11,11 @@ from .permissions import permission_acl_view, permission_acl_edit
def get_kwargs_factory(variable_name):
def get_kwargs(context):
content_type = ContentType.objects.get_for_model(context[variable_name])
return {'app_label': '"{}"'.format(content_type.app_label), 'model': '"{}"'.format(content_type.model), 'object_id': '{}.pk'.format(variable_name)}
return {
'app_label': '"{}"'.format(content_type.app_label),
'model': '"{}"'.format(content_type.model),
'object_id': '{}.pk'.format(variable_name)
}
return get_kwargs

View File

@@ -92,7 +92,13 @@ class AccessControlListManager(models.Manager):
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)})
parent_acl_query = Q(
**{
'{}__pk__in'.format(
parent_accessor
): parent_queryset.values_list('object_id', flat=True)
}
)
else:
parent_acl_query = Q()

View File

@@ -15,12 +15,34 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='AccessEntry',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('holder_id', models.PositiveIntegerField()),
(
'id', models.AutoField(
verbose_name='ID', serialize=False,
auto_created=True, primary_key=True
)
),
(
'holder_id', models.PositiveIntegerField()
),
('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(related_name='object_content_type', to='contenttypes.ContentType')),
('holder_type', models.ForeignKey(related_name='access_holder', to='contenttypes.ContentType')),
('permission', models.ForeignKey(verbose_name='Permission', to='permissions.StoredPermission')),
(
'content_type', models.ForeignKey(
related_name='object_content_type',
to='contenttypes.ContentType'
)
),
(
'holder_type', models.ForeignKey(
related_name='access_holder',
to='contenttypes.ContentType'
)
),
(
'permission', models.ForeignKey(
verbose_name='Permission',
to='permissions.StoredPermission'
)
),
],
options={
'verbose_name': 'Access entry',
@@ -31,7 +53,12 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='CreatorSingleton',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id', models.AutoField(
verbose_name='ID', serialize=False,
auto_created=True, primary_key=True
)
),
],
options={
'verbose_name': 'Creator',
@@ -42,11 +69,31 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='DefaultAccessEntry',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id', models.AutoField(
verbose_name='ID', serialize=False,
auto_created=True, primary_key=True
)
),
('holder_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(related_name='default_access_entry_class', to='contenttypes.ContentType')),
('holder_type', models.ForeignKey(related_name='default_access_entry_holder', to='contenttypes.ContentType')),
('permission', models.ForeignKey(verbose_name='Permission', to='permissions.StoredPermission')),
(
'content_type', models.ForeignKey(
related_name='default_access_entry_class',
to='contenttypes.ContentType'
)
),
(
'holder_type', models.ForeignKey(
related_name='default_access_entry_holder',
to='contenttypes.ContentType'
)
),
(
'permission', models.ForeignKey(
verbose_name='Permission',
to='permissions.StoredPermission'
)
),
],
options={
'verbose_name': 'Default access entry',

View File

@@ -16,11 +16,31 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='AccessControlList',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id', models.AutoField(
verbose_name='ID', serialize=False, auto_created=True,
primary_key=True
)
),
('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(related_name='object_content_type', to='contenttypes.ContentType')),
('permissions', models.ManyToManyField(related_name='acls', verbose_name='Permissions', to='permissions.StoredPermission', blank=True)),
('role', models.ForeignKey(related_name='acls', verbose_name='Role', to='permissions.Role')),
(
'content_type', models.ForeignKey(
related_name='object_content_type',
to='contenttypes.ContentType'
)
),
(
'permissions', models.ManyToManyField(
related_name='acls', verbose_name='Permissions',
to='permissions.StoredPermission', blank=True
)
),
(
'role', models.ForeignKey(
related_name='acls', verbose_name='Role',
to='permissions.Role'
)
),
],
options={
'verbose_name': 'Access entry',

View File

@@ -31,7 +31,10 @@ class AccessControlList(models.Model):
fk_field='object_id',
)
# TODO: limit choices to the permissions valid for the content_object
permissions = models.ManyToManyField(StoredPermission, blank=True, related_name='acls', verbose_name=_('Permissions'))
permissions = models.ManyToManyField(
StoredPermission, blank=True, related_name='acls',
verbose_name=_('Permissions')
)
role = models.ForeignKey(Role, related_name='acls', verbose_name=_('Role'))
objects = AccessControlListManager()
@@ -45,4 +48,6 @@ class AccessControlList(models.Model):
return '{} <=> {}'.format(self.content_object, self.role)
def get_inherited_permissions(self):
return AccessControlList.objects.get_inherited_permissions(role=self.role, obj=self.content_object)
return AccessControlList.objects.get_inherited_permissions(
role=self.role, obj=self.content_object
)

View File

@@ -6,5 +6,9 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('acls', _('Access control lists'))
permission_acl_edit = namespace.add_permission(name='acl_edit', label=_('Edit ACLs'))
permission_acl_view = namespace.add_permission(name='acl_view', label=_('View ACLs'))
permission_acl_edit = namespace.add_permission(
name='acl_edit', label=_('Edit ACLs')
)
permission_acl_view = namespace.add_permission(
name='acl_view', label=_('View ACLs')
)

View File

@@ -17,26 +17,36 @@ from .models import AccessControlList
class PermissionTestCase(TestCase):
def setUp(self):
self.document_type_1 = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
self.document_type_1 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE
)
ocr_settings = self.document_type_1.ocr_settings
ocr_settings.auto_ocr = False
ocr_settings.save()
self.document_type_2 = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE + '2')
self.document_type_2 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE + '2'
)
ocr_settings = self.document_type_2.ocr_settings
ocr_settings.auto_ocr = False
ocr_settings.save()
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
self.document_1 = self.document_type_1.new_document(file_object=File(file_object), label='document 1')
self.document_1 = self.document_type_1.new_document(
file_object=File(file_object), label='document 1'
)
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
self.document_2 = self.document_type_1.new_document(file_object=File(file_object), label='document 2')
self.document_2 = self.document_type_1.new_document(
file_object=File(file_object), label='document 2'
)
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
self.document_3 = self.document_type_2.new_document(file_object=File(file_object), label='document 3')
self.document_3 = self.document_type_2.new_document(
file_object=File(file_object), label='document 3'
)
self.user = get_user_model().objects.create(username='test user')
self.group = Group.objects.create(name='test group')
@@ -52,23 +62,35 @@ class PermissionTestCase(TestCase):
def test_check_access_without_permissions(self):
with self.assertRaises(PermissionDenied):
AccessControlList.objects.check_access(permissions=(permission_document_view,), user=self.user, obj=self.document_1)
AccessControlList.objects.check_access(
permissions=(permission_document_view,),
user=self.user, obj=self.document_1
)
def test_filtering_without_permissions(self):
self.assertEqual(
list(AccessControlList.objects.filter_by_access(permission=permission_document_view, user=self.user, queryset=Document.objects.all())),
[]
list(
AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
)
), []
)
def test_check_access_with_acl(self):
self.group.user_set.add(self.user)
self.role.groups.add(self.group)
acl = AccessControlList.objects.create(content_object=self.document_1, role=self.role)
acl = AccessControlList.objects.create(
content_object=self.document_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(permissions=(permission_document_view,), user=self.user, obj=self.document_1)
AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user,
obj=self.document_1
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
@@ -77,23 +99,34 @@ class PermissionTestCase(TestCase):
self.role.permissions.add(permission_document_view.stored_permission)
self.role.groups.add(self.group)
acl = AccessControlList.objects.create(content_object=self.document_1, role=self.role)
acl = AccessControlList.objects.create(
content_object=self.document_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
self.assertEqual(
list(AccessControlList.objects.filter_by_access(permission=permission_document_view, user=self.user, queryset=Document.objects.all())),
[self.document_1]
list(
AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
)
), [self.document_1]
)
def test_check_access_with_inherited_acl(self):
self.group.user_set.add(self.user)
self.role.groups.add(self.group)
acl = AccessControlList.objects.create(content_object=self.document_type_1, role=self.role)
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(permissions=(permission_document_view,), user=self.user, obj=self.document_1)
AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user,
obj=self.document_1
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
@@ -101,14 +134,21 @@ class PermissionTestCase(TestCase):
self.group.user_set.add(self.user)
self.role.groups.add(self.group)
acl = AccessControlList.objects.create(content_object=self.document_type_1, role=self.role)
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create(content_object=self.document_3, role=self.role)
acl = AccessControlList.objects.create(
content_object=self.document_3, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(permissions=(permission_document_view,), user=self.user, obj=self.document_3)
AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user,
obj=self.document_3
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
@@ -117,10 +157,15 @@ class PermissionTestCase(TestCase):
self.role.permissions.add(permission_document_view.stored_permission)
self.role.groups.add(self.group)
acl = AccessControlList.objects.create(content_object=self.document_type_1, role=self.role)
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access(permission=permission_document_view, user=self.user, queryset=Document.objects.all())
result = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
)
self.assertTrue(self.document_1 in result)
self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 not in result)
@@ -130,13 +175,20 @@ class PermissionTestCase(TestCase):
self.role.permissions.add(permission_document_view.stored_permission)
self.role.groups.add(self.group)
acl = AccessControlList.objects.create(content_object=self.document_type_1, role=self.role)
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create(content_object=self.document_3, role=self.role)
acl = AccessControlList.objects.create(
content_object=self.document_3, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access(permission=permission_document_view, user=self.user, queryset=Document.objects.all())
result = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
)
self.assertTrue(self.document_1 in result)
self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 in result)

View File

@@ -6,8 +6,17 @@ from .views import ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView
urlpatterns = patterns(
'acls.views',
url(r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/new/$', ACLCreateView.as_view(), name='acl_new'),
url(r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/list/$', ACLListView.as_view(), name='acl_list'),
url(
r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/new/$',
ACLCreateView.as_view(), name='acl_new'
),
url(
r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/list/$',
ACLListView.as_view(), name='acl_list'
),
url(r'^(?P<pk>\d+)/delete/$', ACLDeleteView.as_view(), name='acl_delete'),
url(r'^(?P<pk>\d+)/permissions/$', ACLPermissionsView.as_view(), name='acl_permissions'),
url(
r'^(?P<pk>\d+)/permissions/$', ACLPermissionsView.as_view(),
name='acl_permissions'
),
)

View File

@@ -28,25 +28,38 @@ logger = logging.getLogger(__name__)
class ACLListView(SingleObjectListView):
@staticmethod
def permission_titles(permission_list):
return ', '.join([unicode(permission) for permission in permission_list])
return ', '.join(
[unicode(permission) for permission in permission_list]
)
def dispatch(self, request, *args, **kwargs):
self.content_type = get_object_or_404(ContentType, app_label=self.kwargs['app_label'], model=self.kwargs['model'])
self.content_type = get_object_or_404(
ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
try:
self.content_object = self.content_type.get_object_for_this_type(pk=self.kwargs['object_id'])
self.content_object = self.content_type.get_object_for_this_type(
pk=self.kwargs['object_id']
)
except self.content_type.model_class().DoesNotExist:
raise Http404
try:
Permission.check_permissions(request.user, permissions=(permission_acl_view,))
Permission.check_permissions(
request.user, permissions=(permission_acl_view,)
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_acl_view, request.user, self.content_object)
AccessControlList.objects.check_access(
permission_acl_view, request.user, self.content_object
)
return super(ACLListView, self).dispatch(request, *args, **kwargs)
def get_queryset(self):
return AccessControlList.objects.filter(content_type=self.content_type, object_id=self.content_object.pk)
return AccessControlList.objects.filter(
content_type=self.content_type, object_id=self.content_object.pk
)
def get_extra_context(self):
return {
@@ -60,7 +73,11 @@ class ACLListView(SingleObjectListView):
},
{
'name': _('Permissions'),
'attribute': encapsulate(lambda entry: ACLListView.permission_titles(entry.permissions.all()))
'attribute': encapsulate(
lambda entry: ACLListView.permission_titles(
entry.permissions.all()
)
)
},
],
}
@@ -71,17 +88,26 @@ class ACLCreateView(SingleObjectCreateView):
model = AccessControlList
def dispatch(self, request, *args, **kwargs):
content_type = get_object_or_404(ContentType, app_label=self.kwargs['app_label'], model=self.kwargs['model'])
content_type = get_object_or_404(
ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
try:
self.content_object = content_type.get_object_for_this_type(pk=self.kwargs['object_id'])
self.content_object = content_type.get_object_for_this_type(
pk=self.kwargs['object_id']
)
except content_type.model_class().DoesNotExist:
raise Http404
try:
Permission.check_permissions(request.user, permissions=(permission_acl_edit,))
Permission.check_permissions(
request.user, permissions=(permission_acl_edit,)
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_acl_edit, request.user, self.content_object)
AccessControlList.objects.check_access(
permission_acl_edit, request.user, self.content_object
)
return super(ACLCreateView, self).dispatch(request, *args, **kwargs)
@@ -98,7 +124,9 @@ class ACLCreateView(SingleObjectCreateView):
def get_extra_context(self):
return {
'object': self.content_object,
'title': _('New access control lists for: %s' % self.content_object),
'title': _(
'New access control lists for: %s' % self.content_object
),
}
@@ -109,9 +137,13 @@ class ACLDeleteView(SingleObjectDeleteView):
acl = get_object_or_404(AccessControlList, pk=self.kwargs['pk'])
try:
Permission.check_permissions(request.user, permissions=(permission_acl_edit,))
Permission.check_permissions(
request.user, permissions=(permission_acl_edit,)
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_acl_edit, request.user, acl.content_object)
AccessControlList.objects.check_access(
permission_acl_edit, request.user, acl.content_object
)
return super(ACLDeleteView, self).dispatch(request, *args, **kwargs)
@@ -136,8 +168,12 @@ class ACLPermissionsView(AssignRemoveView):
results = []
for namespace, permissions in itertools.groupby(entries, 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
@@ -149,15 +185,23 @@ class ACLPermissionsView(AssignRemoveView):
acl = get_object_or_404(AccessControlList, pk=self.kwargs['pk'])
try:
Permission.check_permissions(request.user, permissions=(permission_acl_edit,))
Permission.check_permissions(
request.user, permissions=(permission_acl_edit,)
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_acl_edit, request.user, acl.content_object)
AccessControlList.objects.check_access(
permission_acl_edit, request.user, acl.content_object
)
return super(ACLPermissionsView, self).dispatch(request, *args, **kwargs)
return super(
ACLPermissionsView, self
).dispatch(request, *args, **kwargs)
def get_right_list_help_text(self):
if self.get_object().get_inherited_permissions():
return _('Disabled permissions are inherited from a parent object.')
return _(
'Disabled permissions are inherited from a parent object.'
)
return None
@@ -165,14 +209,24 @@ class ACLPermissionsView(AssignRemoveView):
return get_object_or_404(AccessControlList, pk=self.kwargs['pk'])
def get_available_list(self):
return ModelPermission.get_for_instance(instance=self.get_object().content_object).exclude(id__in=self.get_granted_list().values_list('pk', flat=True))
return ModelPermission.get_for_instance(
instance=self.get_object().content_object
).exclude(id__in=self.get_granted_list().values_list('pk', flat=True))
def get_disabled_choices(self):
"""
Get permissions from a parent's acls but remove the permissions we
already hold for this object
"""
return map(str, set(self.get_object().get_inherited_permissions().values_list('pk', flat=True)).difference(self.get_object().permissions.values_list('pk', flat=True)))
return map(
str, set(
self.get_object().get_inherited_permissions().values_list(
'pk', flat=True
)
).difference(
self.get_object().permissions.values_list('pk', flat=True)
)
)
def get_extra_context(self):
return {

View File

@@ -8,6 +8,7 @@ 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'
'Controls the mechanism used to authenticated user. Options are: '
'username, email'
)
)

View File

@@ -300,8 +300,10 @@ class TransformationZoom(BaseTransformation):
decimal_value = float(self.percent) / 100
return self.image.resize(
(int(self.image.size[0] * decimal_value),
int(self.image.size[1] * decimal_value)), Image.ANTIALIAS
(
int(self.image.size[0] * decimal_value),
int(self.image.size[1] * decimal_value)
), Image.ANTIALIAS
)

View File

@@ -69,7 +69,9 @@ class TransformationDeleteView(SingleObjectDeleteView):
]
),
'title': _(
'Delete transformation "%(transformation)s" for: %(content_object)s?') % {
'Delete transformation "%(transformation)s" for: '
'%(content_object)s?'
) % {
'transformation': self.transformation,
'content_object': self.transformation.content_object
},

View File

@@ -29,12 +29,19 @@ def comment_delete(request, comment_id=None, comment_id_list=None):
if comment_id:
comments = [get_object_or_404(Comment, pk=comment_id)]
elif comment_id_list:
comments = [get_object_or_404(Comment, pk=comment_id) for comment_id in comment_id_list.split(',')]
comments = [
get_object_or_404(
Comment, pk=comment_id
) for comment_id in comment_id_list.split(',')
]
try:
Permission.check_permissions(request.user, [permission_comment_delete])
except PermissionDenied:
comments = AccessControlList.objects.filter_by_access(permission_comment_delete, request.user, comments, related='content_object')
comments = AccessControlList.objects.filter_by_access(
permission_comment_delete, request.user, comments,
related='content_object'
)
if not comments:
messages.error(request, _('Must provide at least one comment.'))
@@ -47,11 +54,17 @@ def comment_delete(request, comment_id=None, comment_id_list=None):
for comment in comments:
try:
comment.delete()
messages.success(request, _('Comment "%s" deleted successfully.') % comment)
messages.success(
request, _('Comment "%s" deleted successfully.') % comment
)
except Exception as exception:
messages.error(request, _('Error deleting comment "%(comment)s": %(error)s') % {
messages.error(
request, _(
'Error deleting comment "%(comment)s": %(error)s'
) % {
'comment': comment, 'error': exception
})
}
)
return HttpResponseRedirect(next)

View File

@@ -45,9 +45,13 @@ def document_version_post_save_hook(instance):
logger.debug('instance: %s', instance)
try:
document_signature = DocumentVersionSignature.objects.get(document_version=instance)
document_signature = DocumentVersionSignature.objects.get(
document_version=instance
)
except DocumentVersionSignature.DoesNotExist:
document_signature = DocumentVersionSignature.objects.create(document_version=instance)
document_signature = DocumentVersionSignature.objects.create(
document_version=instance
)
document_signature.check_for_embedded_signature()
@@ -60,7 +64,9 @@ class DocumentSignaturesApp(MayanAppConfig):
def ready(self):
super(DocumentSignaturesApp, self).ready()
DocumentVersion.register_post_save_hook(1, document_version_post_save_hook)
DocumentVersion.register_post_save_hook(
1, document_version_post_save_hook
)
DocumentVersion.register_pre_open_hook(1, document_pre_open_hook)
ModelPermission.register(
@@ -70,5 +76,18 @@ class DocumentSignaturesApp(MayanAppConfig):
)
)
menu_facet.bind_links(links=[link_document_verify], sources=[Document])
menu_sidebar.bind_links(links=[link_document_signature_upload, link_document_signature_download, link_document_signature_delete], sources=['signatures:document_verify', 'signatures:document_signature_upload', 'signatures:document_signature_download', 'signatures:document_signature_delete'])
menu_facet.bind_links(
links=[link_document_verify], sources=[Document]
)
menu_sidebar.bind_links(
links=[
link_document_signature_upload,
link_document_signature_download,
link_document_signature_delete
], sources=[
'signatures:document_verify',
'signatures:document_signature_upload',
'signatures:document_signature_download',
'signatures:document_signature_delete'
]
)

View File

@@ -12,14 +12,36 @@ from .permissions import (
def can_upload_detached_signature(context):
return not DocumentVersionSignature.objects.has_detached_signature(context['object'].latest_version) and not DocumentVersionSignature.objects.has_embedded_signature(context['object'].latest_version)
return not DocumentVersionSignature.objects.has_detached_signature(
context['object'].latest_version
) and not DocumentVersionSignature.objects.has_embedded_signature(
context['object'].latest_version
)
def can_delete_detached_signature(context):
return DocumentVersionSignature.objects.has_detached_signature(context['object'].latest_version)
return DocumentVersionSignature.objects.has_detached_signature(
context['object'].latest_version
)
link_document_signature_delete = Link(condition=can_delete_detached_signature, permissions=[permission_signature_delete], tags='dangerous', text=_('Delete signature'), view='signatures:document_signature_delete', args='object.pk')
link_document_signature_download = Link(condition=can_delete_detached_signature, text=_('Download signature'), view='signatures:document_signature_download', args='object.pk', permissions=[permission_signature_download])
link_document_signature_upload = Link(condition=can_upload_detached_signature, permissions=[permission_signature_upload], text=_('Upload signature'), view='signatures:document_signature_upload', args='object.pk')
link_document_verify = Link(permissions=[permission_document_verify], text=_('Signatures'), view='signatures:document_verify', args='object.pk')
link_document_signature_delete = Link(
condition=can_delete_detached_signature,
permissions=[permission_signature_delete], tags='dangerous',
text=_('Delete signature'), view='signatures:document_signature_delete',
args='object.pk'
)
link_document_signature_download = Link(
condition=can_delete_detached_signature, text=_('Download signature'),
view='signatures:document_signature_download', args='object.pk',
permissions=[permission_signature_download]
)
link_document_signature_upload = Link(
condition=can_upload_detached_signature,
permissions=[permission_signature_upload], text=_('Upload signature'),
view='signatures:document_signature_upload', args='object.pk'
)
link_document_verify = Link(
permissions=[permission_document_verify], text=_('Signatures'),
view='signatures:document_verify', args='object.pk'
)

View File

@@ -24,7 +24,9 @@ class DocumentVersionSignatureManager(models.Manager):
)
if document_signature.has_embedded_signature:
raise Exception('Document version already has an embedded signature')
raise Exception(
'Document version already has an embedded signature'
)
else:
if document_signature.signature_file:
logger.debug('Existing detached signature')
@@ -37,7 +39,9 @@ class DocumentVersionSignatureManager(models.Manager):
def has_detached_signature(self, document_version):
try:
document_signature = self.get_document_signature(document_version=document_version)
document_signature = self.get_document_signature(
document_version=document_version
)
except ValueError:
return False
else:
@@ -50,23 +54,31 @@ class DocumentVersionSignatureManager(models.Manager):
logger.debug('document_version: %s', document_version)
try:
document_signature = self.get_document_signature(document_version=document_version)
document_signature = self.get_document_signature(
document_version=document_version
)
except ValueError:
return False
else:
return document_signature.has_embedded_signature
def detached_signature(self, document_version):
document_signature = self.get_document_signature(document_version=document_version)
document_signature = self.get_document_signature(
document_version=document_version
)
return document_signature.signature_file.storage.open(document_signature.signature_file.name)
return document_signature.signature_file.storage.open(
document_signature.signature_file.name
)
def verify_signature(self, document_version):
document_version_descriptor = document_version.open(raw=True)
detached_signature = None
if self.has_detached_signature(document_version=document_version):
logger.debug('has detached signature')
detached_signature = self.detached_signature(document_version=document_version)
detached_signature = self.detached_signature(
document_version=document_version
)
args = (document_version_descriptor, detached_signature)
else:
args = (document_version_descriptor,)
@@ -81,7 +93,9 @@ class DocumentVersionSignatureManager(models.Manager):
detached_signature.close()
def clear_detached_signature(self, document_version):
document_signature = self.get_document_signature(document_version=document_version)
document_signature = self.get_document_signature(
document_version=document_version
)
if not document_signature.signature_file:
raise Exception('document doesn\'t have a detached signature')

View File

@@ -23,9 +23,16 @@ class DocumentVersionSignature(models.Model):
"""
Model that describes a document version signature properties
"""
document_version = models.ForeignKey(DocumentVersion, editable=False, verbose_name=_('Document version'))
signature_file = models.FileField(blank=True, null=True, storage=storage_backend, upload_to=upload_to, verbose_name=_('Signature file'))
has_embedded_signature = models.BooleanField(default=False, verbose_name=_('Has embedded signature'))
document_version = models.ForeignKey(
DocumentVersion, editable=False, verbose_name=_('Document version')
)
signature_file = models.FileField(
blank=True, null=True, storage=storage_backend, upload_to=upload_to,
verbose_name=_('Signature file')
)
has_embedded_signature = models.BooleanField(
default=False, verbose_name=_('Has embedded signature')
)
objects = DocumentVersionSignatureManager()
@@ -33,7 +40,9 @@ class DocumentVersionSignature(models.Model):
logger.debug('checking for embedded signature')
with self.document_version.open(raw=True) as file_object:
self.has_embedded_signature = gpg.has_embedded_signature(file_object)
self.has_embedded_signature = gpg.has_embedded_signature(
file_object
)
self.save()
def delete_detached_signature_file(self):

View File

@@ -4,9 +4,19 @@ from django.utils.translation import ugettext_lazy as _
from permissions import PermissionNamespace
namespace = PermissionNamespace('document_signatures', _('Document signatures'))
namespace = PermissionNamespace(
'document_signatures', _('Document signatures')
)
permission_document_verify = namespace.add_permission(name='document_verify', label=_('Verify document signatures'))
permission_signature_delete = namespace.add_permission(name='signature_delete', label=_('Delete detached signatures'))
permission_signature_download = namespace.add_permission(name='signature_download', label=_('Download detached signatures'))
permission_signature_upload = namespace.add_permission(name='signature_upload', label=_('Upload detached signatures'))
permission_document_verify = namespace.add_permission(
name='document_verify', label=_('Verify document signatures')
)
permission_signature_delete = namespace.add_permission(
name='signature_delete', label=_('Delete detached signatures')
)
permission_signature_download = namespace.add_permission(
name='signature_download', label=_('Download detached signatures')
)
permission_signature_upload = namespace.add_permission(
name='signature_upload', label=_('Upload detached signatures')
)

View File

@@ -5,4 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from smart_settings import Namespace
namespace = Namespace(name='signatures', label=_('Document signatures'))
setting_storage_backend = namespace.add_setting(global_name='SIGNATURES_STORAGE_BACKEND', default='storage.backends.filebasedstorage.FileBasedStorage')
setting_storage_backend = namespace.add_setting(
global_name='SIGNATURES_STORAGE_BACKEND',
default='storage.backends.filebasedstorage.FileBasedStorage'
)

View File

@@ -13,21 +13,32 @@ from django_gpg.runtime import gpg
from .models import DocumentVersionSignature
TEST_SIGNED_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg')
TEST_SIGNATURE_FILE_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.sig')
TEST_KEY_FILE = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'key0x5F3F7F75D210724D.asc')
TEST_SIGNED_DOCUMENT_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg'
)
TEST_SIGNATURE_FILE_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.sig'
)
TEST_KEY_FILE = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents',
'key0x5F3F7F75D210724D.asc'
)
class DocumentTestCase(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()
with open(TEST_DOCUMENT_PATH) as file_object:
self.document = self.document_type.new_document(file_object=File(file_object), label='mayan_11_1.pdf')
self.document = self.document_type.new_document(
file_object=File(file_object), label='mayan_11_1.pdf'
)
with open(TEST_KEY_FILE) as file_object:
gpg.import_key(file_object.read())
@@ -37,24 +48,52 @@ class DocumentTestCase(TestCase):
self.document_type.delete()
def test_document_no_signature(self):
self.assertEqual(DocumentVersionSignature.objects.has_detached_signature(self.document.latest_version), False)
self.assertEqual(
DocumentVersionSignature.objects.has_detached_signature(
self.document.latest_version
), False
)
def test_new_document_version_signed(self):
with open(TEST_SIGNED_DOCUMENT_PATH) as file_object:
self.document.new_version(file_object=File(file_object), comment='test comment 1')
self.document.new_version(
file_object=File(file_object), comment='test comment 1'
)
self.assertEqual(DocumentVersionSignature.objects.has_detached_signature(self.document.latest_version), False)
self.assertEqual(DocumentVersionSignature.objects.verify_signature(self.document.latest_version).status, SIGNATURE_STATE_VALID)
self.assertEqual(
DocumentVersionSignature.objects.has_detached_signature(
self.document.latest_version
), False
)
self.assertEqual(
DocumentVersionSignature.objects.verify_signature(
self.document.latest_version
).status, SIGNATURE_STATE_VALID
)
def test_detached_signatures(self):
with open(TEST_DOCUMENT_PATH) as file_object:
self.document.new_version(file_object=File(file_object), comment='test comment 2')
self.document.new_version(
file_object=File(file_object), comment='test comment 2'
)
# GPGVerificationError
self.assertEqual(DocumentVersionSignature.objects.verify_signature(self.document.latest_version), None)
self.assertEqual(DocumentVersionSignature.objects.verify_signature(
self.document.latest_version), None
)
with open(TEST_SIGNATURE_FILE_PATH, 'rb') as file_object:
DocumentVersionSignature.objects.add_detached_signature(self.document.latest_version, File(file_object))
DocumentVersionSignature.objects.add_detached_signature(
self.document.latest_version, File(file_object)
)
self.assertEqual(DocumentVersionSignature.objects.has_detached_signature(self.document.latest_version), True)
self.assertEqual(DocumentVersionSignature.objects.verify_signature(self.document.latest_version).status, SIGNATURE_STATE_VALID)
self.assertEqual(
DocumentVersionSignature.objects.has_detached_signature(
self.document.latest_version
), True
)
self.assertEqual(
DocumentVersionSignature.objects.verify_signature(
self.document.latest_version
).status, SIGNATURE_STATE_VALID
)

View File

@@ -37,32 +37,122 @@ class DocumentStatesApp(MayanAppConfig):
def ready(self):
super(DocumentStatesApp, self).ready()
SourceColumn(source=Workflow, label=_('Initial state'), attribute=encapsulate(lambda workflow: workflow.get_initial_state() or _('None')))
SourceColumn(
source=Workflow, label=_('Initial state'),
attribute=encapsulate(
lambda workflow: workflow.get_initial_state() or _('None')
)
)
SourceColumn(source=WorkflowInstance, label=_('Current state'), attribute='get_current_state')
SourceColumn(source=WorkflowInstance, label=_('User'), attribute=encapsulate(lambda workflow: getattr(workflow.get_last_log_entry(), 'user', _('None'))))
SourceColumn(source=WorkflowInstance, label=_('Last transition'), attribute='get_last_transition')
SourceColumn(source=WorkflowInstance, label=_('Date and time'), attribute=encapsulate(lambda workflow: getattr(workflow.get_last_log_entry(), 'datetime', _('None'))))
SourceColumn(source=WorkflowInstance, label=_('Completion'), attribute=encapsulate(lambda workflow: getattr(workflow.get_current_state(), 'completion', _('None'))))
SourceColumn(
source=WorkflowInstance, label=_('Current state'),
attribute='get_current_state'
)
SourceColumn(
source=WorkflowInstance, label=_('User'),
attribute=encapsulate(
lambda workflow: getattr(
workflow.get_last_log_entry(), 'user', _('None')
)
)
)
SourceColumn(
source=WorkflowInstance, label=_('Last transition'),
attribute='get_last_transition'
)
SourceColumn(
source=WorkflowInstance, label=_('Date and time'),
attribute=encapsulate(
lambda workflow: getattr(
workflow.get_last_log_entry(), 'datetime', _('None')
)
)
)
SourceColumn(
source=WorkflowInstance, label=_('Completion'),
attribute=encapsulate(lambda workflow: getattr(
workflow.get_current_state(), 'completion', _('None'))
)
)
SourceColumn(source=WorkflowInstanceLogEntry, label=_('Date and time'), attribute='datetime')
SourceColumn(source=WorkflowInstanceLogEntry, label=_('User'), attribute='user')
SourceColumn(source=WorkflowInstanceLogEntry, label=_('Transition'), attribute='transition')
SourceColumn(source=WorkflowInstanceLogEntry, label=_('Comment'), attribute='comment')
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Date and time'),
attribute='datetime'
)
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('User'), attribute='user'
)
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Transition'),
attribute='transition'
)
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Comment'),
attribute='comment'
)
SourceColumn(source=WorkflowState, label=_('Is initial state?'), attribute=encapsulate(lambda state: two_state_template(state.initial)))
SourceColumn(source=WorkflowState, label=_('Completion'), attribute='completion')
SourceColumn(
source=WorkflowState, label=_('Is initial state?'),
attribute=encapsulate(
lambda state: two_state_template(state.initial)
)
)
SourceColumn(
source=WorkflowState, label=_('Completion'), attribute='completion'
)
SourceColumn(source=WorkflowTransition, label=_('Origin state'), attribute='origin_state')
SourceColumn(source=WorkflowTransition, label=_('Destination state'), attribute='destination_state')
SourceColumn(
source=WorkflowTransition, label=_('Origin state'),
attribute='origin_state'
)
SourceColumn(
source=WorkflowTransition, label=_('Destination state'),
attribute='destination_state'
)
menu_facet.bind_links(links=[link_document_workflow_instance_list], sources=[Document])
menu_object.bind_links(links=[link_setup_workflow_states, link_setup_workflow_transitions, link_setup_workflow_document_types, link_setup_workflow_edit, link_setup_workflow_delete], sources=[Workflow])
menu_object.bind_links(links=[link_setup_workflow_state_edit, link_setup_workflow_state_delete], sources=[WorkflowState])
menu_object.bind_links(links=[link_setup_workflow_transition_edit, link_setup_workflow_transition_delete], sources=[WorkflowTransition])
menu_object.bind_links(links=[link_workflow_instance_detail, link_workflow_instance_transition], sources=[WorkflowInstance])
menu_secondary.bind_links(links=[link_setup_workflow_list, link_setup_workflow_create], sources=[Workflow, 'document_states:setup_workflow_create', 'document_states:setup_workflow_list'])
menu_facet.bind_links(
links=[link_document_workflow_instance_list], sources=[Document]
)
menu_object.bind_links(
links=[
link_setup_workflow_states, link_setup_workflow_transitions,
link_setup_workflow_document_types, link_setup_workflow_edit,
link_setup_workflow_delete
], sources=[Workflow]
)
menu_object.bind_links(
links=[
link_setup_workflow_state_edit,
link_setup_workflow_state_delete
], sources=[WorkflowState]
)
menu_object.bind_links(
links=[
link_setup_workflow_transition_edit,
link_setup_workflow_transition_delete
], sources=[WorkflowTransition]
)
menu_object.bind_links(
links=[
link_workflow_instance_detail,
link_workflow_instance_transition
], sources=[WorkflowInstance]
)
menu_secondary.bind_links(
links=[link_setup_workflow_list, link_setup_workflow_create],
sources=[
Workflow, 'document_states:setup_workflow_create',
'document_states:setup_workflow_list'
]
)
menu_setup.bind_links(links=[link_setup_workflow_list])
menu_sidebar.bind_links(links=[link_setup_workflow_state_create, link_setup_workflow_transition_create], sources=[Workflow])
menu_sidebar.bind_links(
links=[
link_setup_workflow_state_create,
link_setup_workflow_transition_create
], sources=[Workflow]
)
post_save.connect(launch_workflow, dispatch_uid='launch_workflow', sender=Document)
post_save.connect(
launch_workflow, dispatch_uid='launch_workflow', sender=Document
)

View File

@@ -4,19 +4,66 @@ from django.utils.translation import ugettext_lazy as _
from navigation import Link
link_document_workflow_instance_list = Link(text=_('Workflows'), view='document_states:document_workflow_instance_list', args='object.pk')
link_setup_workflow_create = Link(text=_('Create workflow'), view='document_states:setup_workflow_create')
link_setup_workflow_delete = Link(tags='dangerous', text=_('Delete'), view='document_states:setup_workflow_delete', args='object.pk')
link_setup_workflow_document_types = Link(text=_('Document types'), view='document_states:setup_workflow_document_types', args='object.pk')
link_setup_workflow_edit = Link(text=_('Edit'), view='document_states:setup_workflow_edit', args='object.pk')
link_setup_workflow_list = Link(icon='fa fa-sitemap', text=_('Workflows'), view='document_states:setup_workflow_list')
link_setup_workflow_state_create = Link(text=_('Create state'), view='document_states:setup_workflow_state_create', args='object.pk')
link_setup_workflow_state_delete = Link(tags='dangerous', text=_('Delete'), view='document_states:setup_workflow_state_delete', args='object.pk')
link_setup_workflow_state_edit = Link(text=_('Edit'), view='document_states:setup_workflow_state_edit', args='object.pk')
link_setup_workflow_states = Link(text=_('States'), view='document_states:setup_workflow_states', args='object.pk')
link_setup_workflow_transition_create = Link(text=_('Create transition'), view='document_states:setup_workflow_transition_create', args='object.pk')
link_setup_workflow_transition_delete = Link(tags='dangerous', text=_('Delete'), view='document_states:setup_workflow_transition_delete', args='object.pk')
link_setup_workflow_transition_edit = Link(text=_('Edit'), view='document_states:setup_workflow_transition_edit', args='object.pk')
link_setup_workflow_transitions = Link(text=_('Transitions'), view='document_states:setup_workflow_transitions', args='object.pk')
link_workflow_instance_detail = Link(text=_('Detail'), view='document_states:workflow_instance_detail', args='resolved_object.pk')
link_workflow_instance_transition = Link(text=_('Transition'), view='document_states:workflow_instance_transition', args='resolved_object.pk')
link_document_workflow_instance_list = Link(
text=_('Workflows'),
view='document_states:document_workflow_instance_list', args='object.pk'
)
link_setup_workflow_create = Link(
text=_('Create workflow'), view='document_states:setup_workflow_create'
)
link_setup_workflow_delete = Link(
tags='dangerous', text=_('Delete'),
view='document_states:setup_workflow_delete', args='object.pk'
)
link_setup_workflow_document_types = Link(
text=_('Document types'),
view='document_states:setup_workflow_document_types', args='object.pk'
)
link_setup_workflow_edit = Link(
text=_('Edit'), view='document_states:setup_workflow_edit',
args='object.pk'
)
link_setup_workflow_list = Link(
icon='fa fa-sitemap', text=_('Workflows'),
view='document_states:setup_workflow_list'
)
link_setup_workflow_state_create = Link(
text=_('Create state'),
view='document_states:setup_workflow_state_create', args='object.pk'
)
link_setup_workflow_state_delete = Link(
tags='dangerous', text=_('Delete'),
view='document_states:setup_workflow_state_delete', args='object.pk'
)
link_setup_workflow_state_edit = Link(
text=_('Edit'), view='document_states:setup_workflow_state_edit',
args='object.pk'
)
link_setup_workflow_states = Link(
text=_('States'), view='document_states:setup_workflow_states',
args='object.pk'
)
link_setup_workflow_transition_create = Link(
text=_('Create transition'),
view='document_states:setup_workflow_transition_create', args='object.pk'
)
link_setup_workflow_transition_delete = Link(
tags='dangerous', text=_('Delete'),
view='document_states:setup_workflow_transition_delete', args='object.pk'
)
link_setup_workflow_transition_edit = Link(
text=_('Edit'), view='document_states:setup_workflow_transition_edit',
args='object.pk'
)
link_setup_workflow_transitions = Link(
text=_('Transitions'), view='document_states:setup_workflow_transitions',
args='object.pk'
)
link_workflow_instance_detail = Link(
text=_('Detail'), view='document_states:workflow_instance_detail',
args='resolved_object.pk'
)
link_workflow_instance_transition = Link(
text=_('Transition'),
view='document_states:workflow_instance_transition', args='resolved_object.pk'
)

View File

@@ -17,8 +17,13 @@ logger = logging.getLogger(__name__)
@python_2_unicode_compatible
class Workflow(models.Model):
label = models.CharField(max_length=255, unique=True, verbose_name=_('Label'))
document_types = models.ManyToManyField(DocumentType, related_name='workflows', verbose_name=_('Document types'))
label = models.CharField(
max_length=255, unique=True, verbose_name=_('Label')
)
document_types = models.ManyToManyField(
DocumentType, related_name='workflows',
verbose_name=_('Document types')
)
objects = WorkflowManager()
@@ -36,12 +41,18 @@ class Workflow(models.Model):
def launch_for(self, document):
try:
logger.info('Launching workflow %s for document %s', self, document)
logger.info(
'Launching workflow %s for document %s', self, document
)
self.instances.create(document=document)
except IntegrityError:
logger.info('Workflow %s already launched for document %s', self, document)
logger.info(
'Workflow %s already launched for document %s', self, document
)
else:
logger.info('Workflow %s launched for document %s', self, document)
logger.info(
'Workflow %s launched for document %s', self, document
)
class Meta:
verbose_name = _('Workflow')
@@ -50,10 +61,23 @@ class Workflow(models.Model):
@python_2_unicode_compatible
class WorkflowState(models.Model):
workflow = models.ForeignKey(Workflow, related_name='states', verbose_name=_('Workflow'))
workflow = models.ForeignKey(
Workflow, related_name='states', verbose_name=_('Workflow')
)
label = models.CharField(max_length=255, verbose_name=_('Label'))
initial = models.BooleanField(default=False, help_text=_('Select if this will be the state with which you want the workflow to start in. Only one state can be the initial state.'), verbose_name=_('Initial'))
completion = models.IntegerField(blank=True, default=0, help_text=_('Enter the percent of completion that this state represents in relation to the workflow. Use numbers without the percent sign.'), verbose_name=_('Completion'))
initial = models.BooleanField(
default=False,
help_text=_(
'Select if this will be the state with which you want the '
'workflow to start in. Only one state can be the initial state.'
), verbose_name=_('Initial')
)
completion = models.IntegerField(
blank=True, default=0, help_text=_(
'Enter the percent of completion that this state represents in '
'relation to the workflow. Use numbers without the percent sign.'
), verbose_name=_('Completion')
)
def __str__(self):
return self.label
@@ -71,36 +95,54 @@ class WorkflowState(models.Model):
@python_2_unicode_compatible
class WorkflowTransition(models.Model):
workflow = models.ForeignKey(Workflow, related_name='transitions', verbose_name=_('Workflow'))
workflow = models.ForeignKey(
Workflow, related_name='transitions', verbose_name=_('Workflow')
)
label = models.CharField(max_length=255, verbose_name=_('Label'))
origin_state = models.ForeignKey(WorkflowState, related_name='origin_transitions', verbose_name=_('Origin state'))
destination_state = models.ForeignKey(WorkflowState, related_name='destination_transitions', verbose_name=_('Destination state'))
origin_state = models.ForeignKey(
WorkflowState, related_name='origin_transitions',
verbose_name=_('Origin state')
)
destination_state = models.ForeignKey(
WorkflowState, related_name='destination_transitions',
verbose_name=_('Destination state')
)
def __str__(self):
return self.label
class Meta:
unique_together = ('workflow', 'label', 'origin_state', 'destination_state')
unique_together = (
'workflow', 'label', 'origin_state', 'destination_state'
)
verbose_name = _('Workflow transition')
verbose_name_plural = _('Workflow transitions')
@python_2_unicode_compatible
class WorkflowInstance(models.Model):
workflow = models.ForeignKey(Workflow, related_name='instances', verbose_name=_('Workflow'))
document = models.ForeignKey(Document, related_name='workflows', verbose_name=_('Document'))
workflow = models.ForeignKey(
Workflow, related_name='instances', verbose_name=_('Workflow')
)
document = models.ForeignKey(
Document, related_name='workflows', verbose_name=_('Document')
)
def __str__(self):
return unicode(self.workflow)
def get_absolute_url(self):
return reverse('document_states:workflow_instance_detail', args=[str(self.pk)])
return reverse(
'document_states:workflow_instance_detail', args=[str(self.pk)]
)
def do_transition(self, comment, transition, user):
try:
if transition in self.get_current_state().origin_transitions.all():
self.log_entries.create(comment=comment, transition=transition, user=user)
self.log_entries.create(
comment=comment, transition=transition, user=user
)
except AttributeError:
# No initial state has been set for this workflow
pass
@@ -134,9 +176,16 @@ class WorkflowInstance(models.Model):
@python_2_unicode_compatible
class WorkflowInstanceLogEntry(models.Model):
workflow_instance = models.ForeignKey(WorkflowInstance, related_name='log_entries', verbose_name=_('Workflow instance'))
datetime = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_('Datetime'))
transition = models.ForeignKey(WorkflowTransition, verbose_name=_('Transition'))
workflow_instance = models.ForeignKey(
WorkflowInstance, related_name='log_entries',
verbose_name=_('Workflow instance')
)
datetime = models.DateTimeField(
auto_now_add=True, db_index=True, verbose_name=_('Datetime')
)
transition = models.ForeignKey(
WorkflowTransition, verbose_name=_('Transition')
)
user = models.ForeignKey(User, verbose_name=_('User'))
comment = models.TextField(blank=True, verbose_name=_('Comment'))

View File

@@ -6,9 +6,22 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('document_states', _('Document workflows'))
permission_workflow_create = namespace.add_permission(name='workflow_create', label=_('Create workflows'))
permission_workflow_delete = namespace.add_permission(name='workflow_delte', label=_('Delete workflows'))
permission_workflow_edit = namespace.add_permission(name='workflow_edit', label=_('Edit workflows'))
permission_workflow_view = namespace.add_permission(name='workflow_view', label=_('View workflows'))
permission_document_workflow_view = namespace.add_permission(name='document_workflow_view', label=_('View document workflows'))
permission_document_workflow_transition = namespace.add_permission(name='document_workflow_transition', label=_('Transition document workflows'))
permission_workflow_create = namespace.add_permission(
name='workflow_create', label=_('Create workflows')
)
permission_workflow_delete = namespace.add_permission(
name='workflow_delte', label=_('Delete workflows')
)
permission_workflow_edit = namespace.add_permission(
name='workflow_edit', label=_('Edit workflows')
)
permission_workflow_view = namespace.add_permission(
name='workflow_view', label=_('View workflows')
)
permission_document_workflow_view = namespace.add_permission(
name='document_workflow_view', label=_('View document workflows')
)
permission_document_workflow_transition = namespace.add_permission(
name='document_workflow_transition',
label=_('Transition document workflows')
)

View File

@@ -16,22 +16,84 @@ from .views import (
urlpatterns = patterns(
'',
url(r'^document/(?P<pk>\d+)/workflows/$', DocumentWorkflowInstanceListView.as_view(), name='document_workflow_instance_list'),
url(r'^document/workflows/(?P<pk>\d+)/$', WorkflowInstanceDetailView.as_view(), name='workflow_instance_detail'),
url(r'^document/workflows/(?P<pk>\d+)/transition/$', WorkflowInstanceTransitionView.as_view(), name='workflow_instance_transition'),
url(
r'^document/(?P<pk>\d+)/workflows/$',
DocumentWorkflowInstanceListView.as_view(),
name='document_workflow_instance_list'
),
url(
r'^document/workflows/(?P<pk>\d+)/$',
WorkflowInstanceDetailView.as_view(), name='workflow_instance_detail'
),
url(
r'^document/workflows/(?P<pk>\d+)/transition/$',
WorkflowInstanceTransitionView.as_view(),
name='workflow_instance_transition'
),
url(r'^setup/all/$', SetupWorkflowListView.as_view(), name='setup_workflow_list'),
url(r'^setup/create/$', SetupWorkflowCreateView.as_view(), name='setup_workflow_create'),
url(r'^setup/(?P<pk>\d+)/edit/$', SetupWorkflowEditView.as_view(), name='setup_workflow_edit'),
url(r'^setup/(?P<pk>\d+)/delete/$', SetupWorkflowDeleteView.as_view(), name='setup_workflow_delete'),
url(r'^setup/(?P<pk>\d+)/documents/$', WorkflowDocumentListView.as_view(), name='setup_workflow_document_list'),
url(r'^setup/(?P<pk>\d+)/document_types/$', SetupWorkflowDocumentTypesView.as_view(), name='setup_workflow_document_types'),
url(r'^setup/(?P<pk>\d+)/states/$', SetupWorkflowStateListView.as_view(), name='setup_workflow_states'),
url(r'^setup/(?P<pk>\d+)/states/create/$', SetupWorkflowStateCreateView.as_view(), name='setup_workflow_state_create'),
url(r'^setup/(?P<pk>\d+)/transitions/$', SetupWorkflowTransitionListView.as_view(), name='setup_workflow_transitions'),
url(r'^setup/(?P<pk>\d+)/transitions/create/$', SetupWorkflowTransitionCreateView.as_view(), name='setup_workflow_transition_create'),
url(r'^setup/workflow/state/(?P<pk>\d+)/delete/$', SetupWorkflowStateDeleteView.as_view(), name='setup_workflow_state_delete'),
url(r'^setup/workflow/state/(?P<pk>\d+)/edit/$', SetupWorkflowStateEditView.as_view(), name='setup_workflow_state_edit'),
url(r'^setup/workflow/transitions/(?P<pk>\d+)/delete/$', SetupWorkflowTransitionDeleteView.as_view(), name='setup_workflow_transition_delete'),
url(r'^setup/workflow/transitions/(?P<pk>\d+)/edit/$', SetupWorkflowTransitionEditView.as_view(), name='setup_workflow_transition_edit'),
url(
r'^setup/all/$', SetupWorkflowListView.as_view(),
name='setup_workflow_list'
),
url(
r'^setup/create/$', SetupWorkflowCreateView.as_view(),
name='setup_workflow_create'
),
url(
r'^setup/(?P<pk>\d+)/edit/$', SetupWorkflowEditView.as_view(),
name='setup_workflow_edit'
),
url(
r'^setup/(?P<pk>\d+)/delete/$', SetupWorkflowDeleteView.as_view(),
name='setup_workflow_delete'
),
url(
r'^setup/(?P<pk>\d+)/documents/$',
WorkflowDocumentListView.as_view(),
name='setup_workflow_document_list'
),
url(
r'^setup/(?P<pk>\d+)/document_types/$',
SetupWorkflowDocumentTypesView.as_view(),
name='setup_workflow_document_types'
),
url(
r'^setup/(?P<pk>\d+)/states/$', SetupWorkflowStateListView.as_view(),
name='setup_workflow_states'
),
url(
r'^setup/(?P<pk>\d+)/states/create/$',
SetupWorkflowStateCreateView.as_view(),
name='setup_workflow_state_create'
),
url(
r'^setup/(?P<pk>\d+)/transitions/$',
SetupWorkflowTransitionListView.as_view(),
name='setup_workflow_transitions'
),
url(
r'^setup/(?P<pk>\d+)/transitions/create/$',
SetupWorkflowTransitionCreateView.as_view(),
name='setup_workflow_transition_create'
),
url(
r'^setup/workflow/state/(?P<pk>\d+)/delete/$',
SetupWorkflowStateDeleteView.as_view(),
name='setup_workflow_state_delete'
),
url(
r'^setup/workflow/state/(?P<pk>\d+)/edit/$',
SetupWorkflowStateEditView.as_view(),
name='setup_workflow_state_edit'
),
url(
r'^setup/workflow/transitions/(?P<pk>\d+)/delete/$',
SetupWorkflowTransitionDeleteView.as_view(),
name='setup_workflow_transition_delete'
),
url(
r'^setup/workflow/transitions/(?P<pk>\d+)/edit/$',
SetupWorkflowTransitionEditView.as_view(),
name='setup_workflow_transition_edit'
),
)

View File

@@ -33,11 +33,18 @@ from .permissions import (
class DocumentWorkflowInstanceListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
try:
Permission.check_permissions(request.user, [permission_document_workflow_view])
Permission.check_permissions(
request.user, [permission_document_workflow_view]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_document_workflow_view, request.user, self.get_document())
AccessControlList.objects.check_access(
permission_document_workflow_view, request.user,
self.get_document()
)
return super(DocumentWorkflowInstanceListView, self).dispatch(request, *args, **kwargs)
return super(
DocumentWorkflowInstanceListView, self
).dispatch(request, *args, **kwargs)
def get_document(self):
return get_object_or_404(Document, pk=self.kwargs['pk'])
@@ -46,12 +53,16 @@ class DocumentWorkflowInstanceListView(SingleObjectListView):
return self.get_document().workflows.all()
def get_context_data(self, **kwargs):
context = super(DocumentWorkflowInstanceListView, self).get_context_data(**kwargs)
context = super(
DocumentWorkflowInstanceListView, self
).get_context_data(**kwargs)
context.update(
{
'hide_link': True,
'object': self.get_document(),
'title': _('Workflows for document: %s') % self.get_document(),
'title': _(
'Workflows for document: %s'
) % self.get_document(),
}
)
@@ -63,14 +74,22 @@ class WorkflowDocumentListView(DocumentListView):
self.workflow = get_object_or_404(Workflow, pk=self.kwargs['pk'])
try:
Permission.check_permissions(request.user, [permission_workflow_view])
Permission.check_permissions(
request.user, [permission_workflow_view]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_workflow_view, request.user, self.workflow)
AccessControlList.objects.check_access(
permission_workflow_view, request.user, self.workflow
)
return super(WorkflowDocumentListView, self).dispatch(request, *args, **kwargs)
return super(
WorkflowDocumentListView, self
).dispatch(request, *args, **kwargs)
def get_document_queryset(self):
return Document.objects.filter(document_type__in=self.workflow.document_types.all())
return Document.objects.filter(
document_type__in=self.workflow.document_types.all()
)
def get_extra_context(self):
return {
@@ -83,11 +102,18 @@ class WorkflowDocumentListView(DocumentListView):
class WorkflowInstanceDetailView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
try:
Permission.check_permissions(request.user, [permission_document_workflow_view])
Permission.check_permissions(
request.user, [permission_document_workflow_view]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_document_workflow_view, request.user, self.get_workflow_instance().document)
AccessControlList.objects.check_access(
permission_document_workflow_view, request.user,
self.get_workflow_instance().document
)
return super(WorkflowInstanceDetailView, self).dispatch(request, *args, **kwargs)
return super(
WorkflowInstanceDetailView, self
).dispatch(request, *args, **kwargs)
def get_workflow_instance(self):
return get_object_or_404(WorkflowInstance, pk=self.kwargs['pk'])
@@ -116,15 +142,27 @@ class WorkflowInstanceTransitionView(FormView):
def dispatch(self, request, *args, **kwargs):
try:
Permission.check_permissions(request.user, [permission_document_workflow_transition])
Permission.check_permissions(
request.user, [permission_document_workflow_transition]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_document_workflow_transition, request.user, self.get_workflow_instance().document)
AccessControlList.objects.check_access(
permission_document_workflow_transition, request.user,
self.get_workflow_instance().document
)
return super(WorkflowInstanceTransitionView, self).dispatch(request, *args, **kwargs)
return super(
WorkflowInstanceTransitionView, self
).dispatch(request, *args, **kwargs)
def form_valid(self, form):
transition = self.get_workflow_instance().workflow.transitions.get(pk=form.cleaned_data['transition'])
self.get_workflow_instance().do_transition(comment=form.cleaned_data['comment'], transition=transition, user=self.request.user)
transition = self.get_workflow_instance().workflow.transitions.get(
pk=form.cleaned_data['transition']
)
self.get_workflow_instance().do_transition(
comment=form.cleaned_data['comment'], transition=transition,
user=self.request.user
)
return HttpResponseRedirect(self.get_success_url())
def get_form_kwargs(self):
@@ -136,14 +174,18 @@ class WorkflowInstanceTransitionView(FormView):
return get_object_or_404(WorkflowInstance, pk=self.kwargs['pk'])
def get_context_data(self, **kwargs):
context = super(WorkflowInstanceTransitionView, self).get_context_data(**kwargs)
context = super(
WorkflowInstanceTransitionView, self
).get_context_data(**kwargs)
context.update(
{
'navigation_object_list': ['object', 'workflow_instance'],
'object': self.get_workflow_instance().document,
'submit_label': _('Submit'),
'title': _('Do transition for workflow: %s') % self.get_workflow_instance(),
'title': _(
'Do transition for workflow: %s'
) % self.get_workflow_instance(),
'workflow_instance': self.get_workflow_instance(),
}
)
@@ -193,17 +235,21 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView):
def add(self, item):
self.get_object().document_types.add(item)
# TODO: add task launching this workflow for all the document types of
# item
# TODO: add task launching this workflow for all the document types
# of item
def get_object(self):
return get_object_or_404(Workflow, pk=self.kwargs['pk'])
def left_list(self):
return AssignRemoveView.generate_choices(self.get_object().get_document_types_not_in_workflow())
return AssignRemoveView.generate_choices(
self.get_object().get_document_types_not_in_workflow()
)
def right_list(self):
return AssignRemoveView.generate_choices(self.get_object().document_types.all())
return AssignRemoveView.generate_choices(
self.get_object().document_types.all()
)
def remove(self, item):
self.get_object().document_types.remove(item)
@@ -211,9 +257,13 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView):
# item
def get_context_data(self, **kwargs):
data = super(SetupWorkflowDocumentTypesView, self).get_context_data(**kwargs)
data = super(
SetupWorkflowDocumentTypesView, self
).get_context_data(**kwargs)
data.update({
'title': _('Document types assigned the workflow: %s') % self.get_object(),
'title': _(
'Document types assigned the workflow: %s'
) % self.get_object(),
'object': self.get_object(),
})
@@ -223,11 +273,17 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView):
class SetupWorkflowStateListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
try:
Permission.check_permissions(request.user, [permission_workflow_edit])
Permission.check_permissions(
request.user, [permission_workflow_edit]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_workflow_edit, request.user, self.get_workflow())
AccessControlList.objects.check_access(
permission_workflow_edit, request.user, self.get_workflow()
)
return super(SetupWorkflowStateListView, self).dispatch(request, *args, **kwargs)
return super(
SetupWorkflowStateListView, self
).dispatch(request, *args, **kwargs)
def get_workflow(self):
return get_object_or_404(Workflow, pk=self.kwargs['pk'])
@@ -236,7 +292,9 @@ class SetupWorkflowStateListView(SingleObjectListView):
return self.get_workflow().states.all()
def get_context_data(self, **kwargs):
context = super(SetupWorkflowStateListView, self).get_context_data(**kwargs)
context = super(
SetupWorkflowStateListView, self
).get_context_data(**kwargs)
context.update(
{
'hide_link': True,
@@ -253,18 +311,28 @@ class SetupWorkflowStateCreateView(SingleObjectCreateView):
def dispatch(self, request, *args, **kwargs):
try:
Permission.check_permissions(request.user, [permission_workflow_edit])
Permission.check_permissions(
request.user, [permission_workflow_edit]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_workflow_edit, request.user, self.get_workflow())
AccessControlList.objects.check_access(
permission_workflow_edit, request.user, self.get_workflow()
)
return super(SetupWorkflowStateCreateView, self).dispatch(request, *args, **kwargs)
return super(
SetupWorkflowStateCreateView, self
).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(SetupWorkflowStateCreateView, self).get_context_data(**kwargs)
context = super(
SetupWorkflowStateCreateView, self
).get_context_data(**kwargs)
context.update(
{
'object': self.get_workflow(),
'title': _('Create states for workflow: %s') % self.get_workflow()
'title': _(
'Create states for workflow: %s'
) % self.get_workflow()
}
)
return context
@@ -276,7 +344,9 @@ class SetupWorkflowStateCreateView(SingleObjectCreateView):
return self.get_workflow().states.all()
def get_success_url(self):
return reverse('document_states:setup_workflow_states', args=[self.kwargs['pk']])
return reverse(
'document_states:setup_workflow_states', args=[self.kwargs['pk']]
)
def form_valid(self, form):
self.object = form.save(commit=False)
@@ -290,7 +360,9 @@ class SetupWorkflowStateDeleteView(SingleObjectDeleteView):
view_permission = permission_workflow_delete
def get_context_data(self, **kwargs):
context = super(SetupWorkflowStateDeleteView, self).get_context_data(**kwargs)
context = super(
SetupWorkflowStateDeleteView, self
).get_context_data(**kwargs)
context.update(
{
@@ -303,7 +375,10 @@ class SetupWorkflowStateDeleteView(SingleObjectDeleteView):
return context
def get_success_url(self):
return reverse('document_states:setup_workflow_States', args=[self.get_object().workflow.pk])
return reverse(
'document_states:setup_workflow_States',
args=[self.get_object().workflow.pk]
)
class SetupWorkflowStateEditView(SingleObjectEditView):
@@ -312,7 +387,9 @@ class SetupWorkflowStateEditView(SingleObjectEditView):
view_permission = permission_workflow_edit
def get_context_data(self, **kwargs):
context = super(SetupWorkflowStateEditView, self).get_context_data(**kwargs)
context = super(
SetupWorkflowStateEditView, self
).get_context_data(**kwargs)
context.update(
{
@@ -325,7 +402,10 @@ class SetupWorkflowStateEditView(SingleObjectEditView):
return context
def get_success_url(self):
return reverse('document_states:setup_workflow_states', args=[self.get_object().workflow.pk])
return reverse(
'document_states:setup_workflow_states',
args=[self.get_object().workflow.pk]
)
# Transitions
@@ -334,11 +414,17 @@ class SetupWorkflowStateEditView(SingleObjectEditView):
class SetupWorkflowTransitionListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
try:
Permission.check_permissions(request.user, [permission_workflow_edit])
Permission.check_permissions(
request.user, [permission_workflow_edit]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_workflow_edit, request.user, self.get_workflow())
AccessControlList.objects.check_access(
permission_workflow_edit, request.user, self.get_workflow()
)
return super(SetupWorkflowTransitionListView, self).dispatch(request, *args, **kwargs)
return super(
SetupWorkflowTransitionListView, self
).dispatch(request, *args, **kwargs)
def get_workflow(self):
return get_object_or_404(Workflow, pk=self.kwargs['pk'])
@@ -347,12 +433,16 @@ class SetupWorkflowTransitionListView(SingleObjectListView):
return self.get_workflow().transitions.all()
def get_context_data(self, **kwargs):
context = super(SetupWorkflowTransitionListView, self).get_context_data(**kwargs)
context = super(
SetupWorkflowTransitionListView, self
).get_context_data(**kwargs)
context.update(
{
'hide_link': True,
'object': self.get_workflow(),
'title': _('Transitions of workflow: %s') % self.get_workflow()
'title': _(
'Transitions of workflow: %s'
) % self.get_workflow()
}
)
@@ -364,24 +454,36 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView):
def dispatch(self, request, *args, **kwargs):
try:
Permission.check_permissions(request.user, [permission_workflow_edit])
Permission.check_permissions(
request.user, [permission_workflow_edit]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_workflow_edit, request.user, self.get_workflow())
AccessControlList.objects.check_access(
permission_workflow_edit, request.user, self.get_workflow()
)
return super(SetupWorkflowTransitionCreateView, self).dispatch(request, *args, **kwargs)
return super(
SetupWorkflowTransitionCreateView, self
).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(SetupWorkflowTransitionCreateView, self).get_context_data(**kwargs)
context = super(
SetupWorkflowTransitionCreateView, self
).get_context_data(**kwargs)
context.update(
{
'object': self.get_workflow(),
'title': _('Create transitions for workflow: %s') % self.get_workflow()
'title': _(
'Create transitions for workflow: %s'
) % self.get_workflow()
}
)
return context
def get_form_kwargs(self):
kwargs = super(SetupWorkflowTransitionCreateView, self).get_form_kwargs()
kwargs = super(
SetupWorkflowTransitionCreateView, self
).get_form_kwargs()
kwargs['workflow'] = self.get_workflow()
return kwargs
@@ -392,7 +494,10 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView):
return self.get_workflow().transitions.all()
def get_success_url(self):
return reverse('document_states:setup_workflow_transitions', args=[self.kwargs['pk']])
return reverse(
'document_states:setup_workflow_transitions',
args=[self.kwargs['pk']]
)
def form_valid(self, form):
self.object = form.save(commit=False)
@@ -400,8 +505,12 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView):
try:
self.object.save()
except IntegrityError:
messages.error(self.request, _('Unable to save transition; integrity error.'))
return super(SetupWorkflowTransitionCreateView, self).form_invalid(form)
messages.error(
self.request, _('Unable to save transition; integrity error.')
)
return super(
SetupWorkflowTransitionCreateView, self
).form_invalid(form)
else:
return HttpResponseRedirect(self.get_success_url())
@@ -411,7 +520,9 @@ class SetupWorkflowTransitionDeleteView(SingleObjectDeleteView):
view_permission = permission_workflow_delete
def get_context_data(self, **kwargs):
context = super(SetupWorkflowTransitionDeleteView, self).get_context_data(**kwargs)
context = super(
SetupWorkflowTransitionDeleteView, self
).get_context_data(**kwargs)
context.update(
{
@@ -424,7 +535,10 @@ class SetupWorkflowTransitionDeleteView(SingleObjectDeleteView):
return context
def get_success_url(self):
return reverse('document_states:setup_workflow_transitions', args=[self.get_object().workflow.pk])
return reverse(
'document_states:setup_workflow_transitions',
args=[self.get_object().workflow.pk]
)
class SetupWorkflowTransitionEditView(SingleObjectEditView):
@@ -433,7 +547,9 @@ class SetupWorkflowTransitionEditView(SingleObjectEditView):
view_permission = permission_workflow_edit
def get_context_data(self, **kwargs):
context = super(SetupWorkflowTransitionEditView, self).get_context_data(**kwargs)
context = super(
SetupWorkflowTransitionEditView, self
).get_context_data(**kwargs)
context.update(
{
@@ -446,9 +562,14 @@ class SetupWorkflowTransitionEditView(SingleObjectEditView):
return context
def get_form_kwargs(self):
kwargs = super(SetupWorkflowTransitionEditView, self).get_form_kwargs()
kwargs = super(
SetupWorkflowTransitionEditView, self
).get_form_kwargs()
kwargs['workflow'] = self.get_object().workflow
return kwargs
def get_success_url(self):
return reverse('document_states:setup_workflow_transitions', args=[self.get_object().workflow.pk])
return reverse(
'document_states:setup_workflow_transitions',
args=[self.get_object().workflow.pk]
)

View File

@@ -162,9 +162,9 @@ class APIDocumentVersionCreateView(generics.CreateAPIView):
)
if serializer.is_valid():
# Nested resource we take the document pk from the URL and insert it
# so that it needs not to be specified by the user, we mark it as
# a read only field in the serializer
# Nested resource we take the document pk from the URL and insert
# it so that it needs not to be specified by the user, we mark
# it as a read only field in the serializer
document = get_object_or_404(Document, pk=kwargs['pk'])
document.new_version(
@@ -361,7 +361,8 @@ class APIDocumentTypeDocumentListView(generics.ListAPIView):
)
except PermissionDenied:
AccessControlList.objects.check_access(
permission_document_type_view, self.request.user, document_type
permission_document_type_view, self.request.user,
document_type
)
return document_type.documents.all()

View File

@@ -84,8 +84,10 @@ class DocumentsApp(MayanAppConfig):
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(),
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'
)

View File

@@ -42,7 +42,9 @@ class DocumentPreviewForm(forms.Form):
super(DocumentPreviewForm, self).__init__(*args, **kwargs)
self.fields['preview'].initial = document
try:
self.fields['preview'].label = _('Document pages (%d)') % document.page_count
self.fields['preview'].label = _(
'Document pages (%d)'
) % document.page_count
except AttributeError:
self.fields['preview'].label = _('Document pages (%d)') % 0
@@ -62,13 +64,16 @@ class DocumentForm(forms.ModelForm):
super(DocumentForm, self).__init__(*args, **kwargs)
# Is a document (documents app edit) and has been saved (sources app upload)?
# Is a document (documents app edit) and has been saved (sources
# app upload)?
if self.instance and self.instance.pk:
document_type = self.instance.document_type
filenames_qs = document_type.filenames.filter(enabled=True)
if filenames_qs.count():
self.fields['document_type_available_filenames'] = forms.ModelChoiceField(
self.fields[
'document_type_available_filenames'
] = forms.ModelChoiceField(
queryset=filenames_qs,
required=False,
label=_('Quick document rename')
@@ -119,12 +124,20 @@ 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.')
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.')
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):

View File

@@ -38,67 +38,229 @@ def is_min_zoom(context):
# Facet
link_document_preview = Link(permissions=[permission_document_view], text=_('Preview'), view='documents:document_preview', args='object.id')
link_document_properties = Link(permissions=[permission_document_view], text=_('Properties'), view='documents:document_properties', args='object.id')
link_document_version_list = Link(permissions=[permission_document_view], text=_('Versions'), view='documents:document_version_list', args='object.pk')
link_document_pages = Link(permissions=[permission_document_view], text=_('Pages'), view='documents:document_pages', args='resolved_object.pk')
link_document_preview = Link(
permissions=[permission_document_view], text=_('Preview'),
view='documents:document_preview', args='object.id'
)
link_document_properties = Link(
permissions=[permission_document_view], text=_('Properties'),
view='documents:document_properties', args='object.id'
)
link_document_version_list = Link(
permissions=[permission_document_view], text=_('Versions'),
view='documents:document_version_list', args='object.pk'
)
link_document_pages = Link(
permissions=[permission_document_view], text=_('Pages'),
view='documents:document_pages', args='resolved_object.pk'
)
# Actions
link_document_clear_transformations = Link(permissions=[permission_transformation_delete], text=_('Clear transformations'), view='documents:document_clear_transformations', args='object.id')
link_document_delete = Link(permissions=[permission_document_delete], tags='dangerous', text=_('Delete'), view='documents:document_delete', args='object.id')
link_document_trash = Link(permissions=[permission_document_trash], tags='dangerous', text=_('Move to trash'), view='documents:document_trash', args='object.id')
link_document_edit = Link(permissions=[permission_document_properties_edit], text=_('Edit properties'), view='documents:document_edit', args='object.id')
link_document_document_type_edit = Link(permissions=[permission_document_properties_edit], text=_('Change type'), view='documents:document_document_type_edit', args='object.id')
link_document_download = Link(permissions=[permission_document_download], text=_('Download'), view='documents:document_download', args='object.id')
link_document_print = Link(permissions=[permission_document_print], text=_('Print'), view='documents:document_print', args='object.id')
link_document_update_page_count = Link(permissions=[permission_document_tools], text=_('Reset page count'), view='documents:document_update_page_count', args='object.pk')
link_document_restore = Link(permissions=[permission_document_restore], text=_('Restore'), view='documents:document_restore', args='object.pk')
link_document_multiple_clear_transformations = Link(permissions=[permission_transformation_delete], text=_('Clear transformations'), view='documents:document_multiple_clear_transformations')
link_document_multiple_trash = Link(permissions=[permission_document_trash], tags='dangerous', text=_('Move to trash'), view='documents:document_multiple_trash')
link_document_multiple_delete = Link(permissions=[permission_document_delete], tags='dangerous', text=_('Delete'), view='documents:document_multiple_delete')
link_document_multiple_document_type_edit = Link(permissions=[permission_document_properties_edit], text=_('Change type'), view='documents:document_multiple_document_type_edit')
link_document_multiple_download = Link(permissions=[permission_document_download], text=_('Download'), view='documents:document_multiple_download')
link_document_multiple_update_page_count = Link(permissions=[permission_document_tools], text=_('Reset page count'), view='documents:document_multiple_update_page_count')
link_document_multiple_restore = Link(permissions=[permission_document_restore], text=_('Restore'), view='documents:document_multiple_restore')
link_document_version_download = Link(args='object.pk', permissions=[permission_document_download], text=_('Download'), view='documents:document_version_download')
link_document_clear_transformations = Link(
permissions=[permission_transformation_delete],
text=_('Clear transformations'),
view='documents:document_clear_transformations', args='object.id'
)
link_document_delete = Link(
permissions=[permission_document_delete], tags='dangerous',
text=_('Delete'), view='documents:document_delete', args='object.id'
)
link_document_trash = Link(
permissions=[permission_document_trash], tags='dangerous',
text=_('Move to trash'), view='documents:document_trash', args='object.id'
)
link_document_edit = Link(
permissions=[permission_document_properties_edit],
text=_('Edit properties'), view='documents:document_edit',
args='object.id'
)
link_document_document_type_edit = Link(
permissions=[permission_document_properties_edit], text=_('Change type'),
view='documents:document_document_type_edit', args='object.id'
)
link_document_download = Link(
permissions=[permission_document_download], text=_('Download'),
view='documents:document_download', args='object.id'
)
link_document_print = Link(
permissions=[permission_document_print], text=_('Print'),
view='documents:document_print', args='object.id'
)
link_document_update_page_count = Link(
permissions=[permission_document_tools], text=_('Reset page count'),
view='documents:document_update_page_count', args='object.pk'
)
link_document_restore = Link(
permissions=[permission_document_restore], text=_('Restore'),
view='documents:document_restore', args='object.pk'
)
link_document_multiple_clear_transformations = Link(
permissions=[permission_transformation_delete],
text=_('Clear transformations'),
view='documents:document_multiple_clear_transformations'
)
link_document_multiple_trash = Link(
permissions=[permission_document_trash], tags='dangerous',
text=_('Move to trash'), view='documents:document_multiple_trash'
)
link_document_multiple_delete = Link(
permissions=[permission_document_delete], tags='dangerous',
text=_('Delete'), view='documents:document_multiple_delete'
)
link_document_multiple_document_type_edit = Link(
permissions=[permission_document_properties_edit], text=_('Change type'),
view='documents:document_multiple_document_type_edit'
)
link_document_multiple_download = Link(
permissions=[permission_document_download], text=_('Download'),
view='documents:document_multiple_download'
)
link_document_multiple_update_page_count = Link(
permissions=[permission_document_tools], text=_('Reset page count'),
view='documents:document_multiple_update_page_count'
)
link_document_multiple_restore = Link(
permissions=[permission_document_restore], text=_('Restore'),
view='documents:document_multiple_restore'
)
link_document_version_download = Link(
args='object.pk', permissions=[permission_document_download],
text=_('Download'), view='documents:document_version_download'
)
# Views
link_document_list = Link(icon='fa fa-file', text=_('All documents'), view='documents:document_list')
link_document_list_recent = Link(icon='fa fa-clock-o', text=_('Recent documents'), view='documents:document_list_recent')
link_document_list_deleted = Link(icon='fa fa-trash', text=_('Trash'), view='documents:document_list_deleted')
link_document_list = Link(
icon='fa fa-file', text=_('All documents'), view='documents:document_list'
)
link_document_list_recent = Link(
icon='fa fa-clock-o', text=_('Recent documents'),
view='documents:document_list_recent'
)
link_document_list_deleted = Link(
icon='fa fa-trash', text=_('Trash'),
view='documents:document_list_deleted'
)
# Tools
link_clear_image_cache = Link(
icon='fa fa-file-image-o',
description=_('Clear the graphics representations used to speed up the documents\' display and interactive transformations results.'),
description=_(
'Clear the graphics representations used to speed up the documents\' '
'display and interactive transformations results.'
),
permissions=[permission_document_tools], text=_('Clear document cache'),
view='documents:document_clear_image_cache'
)
link_trash_can_empty = Link(permissions=[permission_empty_trash], text=_('Empty trash'), view='documents:trash_can_empty')
link_trash_can_empty = Link(
permissions=[permission_empty_trash], text=_('Empty trash'),
view='documents:trash_can_empty'
)
# Document pages
link_document_page_navigation_first = Link(conditional_disable=is_first_page, icon='fa fa-step-backward', keep_query=True, permissions=[permission_document_view], text=_('First page'), view='documents:document_page_navigation_first', args='resolved_object.pk')
link_document_page_navigation_last = Link(conditional_disable=is_last_page, icon='fa fa-step-forward', keep_query=True, text=_('Last page'), permissions=[permission_document_view], view='documents:document_page_navigation_last', args='resolved_object.pk')
link_document_page_navigation_previous = Link(conditional_disable=is_first_page, icon='fa fa-arrow-left', keep_query=True, permissions=[permission_document_view], text=_('Previous page'), view='documents:document_page_navigation_previous', args='resolved_object.pk')
link_document_page_navigation_next = Link(conditional_disable=is_last_page, icon='fa fa-arrow-right', keep_query=True, text=_('Next page'), permissions=[permission_document_view], view='documents:document_page_navigation_next', args='resolved_object.pk')
link_document_page_return = Link(icon='fa fa-file', permissions=[permission_document_view], text=_('Document'), view='documents:document_preview', args='resolved_object.document.pk')
link_document_page_rotate_left = Link(icon='fa fa-rotate-left', permissions=[permission_document_view], text=_('Rotate left'), view='documents:document_page_rotate_left', args='resolved_object.pk')
link_document_page_rotate_right = Link(icon='fa fa-rotate-right', permissions=[permission_document_view], text=_('Rotate right'), view='documents:document_page_rotate_right', args='resolved_object.pk')
link_document_page_view = Link(permissions=[permission_document_view], text=_('Page image'), view='documents:document_page_view', args='resolved_object.pk')
link_document_page_view_reset = Link(permissions=[permission_document_view], text=_('Reset view'), view='documents:document_page_view_reset', args='resolved_object.pk')
link_document_page_zoom_in = Link(conditional_disable=is_max_zoom, icon='fa fa-search-plus', permissions=[permission_document_view], text=_('Zoom in'), view='documents:document_page_zoom_in', args='resolved_object.pk')
link_document_page_zoom_out = Link(conditional_disable=is_min_zoom, icon='fa fa-search-minus', permissions=[permission_document_view], text=_('Zoom out'), view='documents:document_page_zoom_out', args='resolved_object.pk')
link_document_page_navigation_first = Link(
conditional_disable=is_first_page, icon='fa fa-step-backward',
keep_query=True, permissions=[permission_document_view],
text=_('First page'), view='documents:document_page_navigation_first',
args='resolved_object.pk'
)
link_document_page_navigation_last = Link(
conditional_disable=is_last_page, icon='fa fa-step-forward',
keep_query=True, text=_('Last page'),
permissions=[permission_document_view],
view='documents:document_page_navigation_last', args='resolved_object.pk'
)
link_document_page_navigation_previous = Link(
conditional_disable=is_first_page, icon='fa fa-arrow-left',
keep_query=True, permissions=[permission_document_view],
text=_('Previous page'),
view='documents:document_page_navigation_previous',
args='resolved_object.pk'
)
link_document_page_navigation_next = Link(
conditional_disable=is_last_page, icon='fa fa-arrow-right',
keep_query=True, text=_('Next page'),
permissions=[permission_document_view],
view='documents:document_page_navigation_next', args='resolved_object.pk'
)
link_document_page_return = Link(
icon='fa fa-file', permissions=[permission_document_view],
text=_('Document'), view='documents:document_preview',
args='resolved_object.document.pk'
)
link_document_page_rotate_left = Link(
icon='fa fa-rotate-left', permissions=[permission_document_view],
text=_('Rotate left'), view='documents:document_page_rotate_left',
args='resolved_object.pk'
)
link_document_page_rotate_right = Link(
icon='fa fa-rotate-right', permissions=[permission_document_view],
text=_('Rotate right'), view='documents:document_page_rotate_right',
args='resolved_object.pk'
)
link_document_page_view = Link(
permissions=[permission_document_view], text=_('Page image'),
view='documents:document_page_view', args='resolved_object.pk'
)
link_document_page_view_reset = Link(
permissions=[permission_document_view], text=_('Reset view'),
view='documents:document_page_view_reset', args='resolved_object.pk'
)
link_document_page_zoom_in = Link(
conditional_disable=is_max_zoom, icon='fa fa-search-plus',
permissions=[permission_document_view], text=_('Zoom in'),
view='documents:document_page_zoom_in', args='resolved_object.pk'
)
link_document_page_zoom_out = Link(
conditional_disable=is_min_zoom, icon='fa fa-search-minus',
permissions=[permission_document_view], text=_('Zoom out'),
view='documents:document_page_zoom_out', args='resolved_object.pk'
)
# Document versions
link_document_version_revert = Link(condition=is_not_current_version, permissions=[permission_document_version_revert], tags='dangerous', text=_('Revert'), view='documents:document_version_revert', args='object.pk')
link_document_version_revert = Link(
condition=is_not_current_version,
permissions=[permission_document_version_revert], tags='dangerous',
text=_('Revert'), view='documents:document_version_revert',
args='object.pk'
)
# Document type related links
link_document_type_create = Link(permissions=[permission_document_type_create], text=_('Create document type'), view='documents:document_type_create')
link_document_type_delete = Link(permissions=[permission_document_type_delete], tags='dangerous', text=_('Delete'), view='documents:document_type_delete', args='resolved_object.id')
link_document_type_edit = Link(permissions=[permission_document_type_edit], text=_('Edit'), view='documents:document_type_edit', args='resolved_object.id')
link_document_type_filename_create = Link(permissions=[permission_document_type_edit], text=_('Add filename to document type'), view='documents:document_type_filename_create', args='document_type.id')
link_document_type_filename_delete = Link(permissions=[permission_document_type_edit], tags='dangerous', text=_('Delete'), view='documents:document_type_filename_delete', args='resolved_object.id')
link_document_type_filename_edit = Link(permissions=[permission_document_type_edit], text=_('Edit'), view='documents:document_type_filename_edit', args='resolved_object.id')
link_document_type_filename_list = Link(permissions=[permission_document_type_view], text=_('Filenames'), view='documents:document_type_filename_list', args='resolved_object.id')
link_document_type_list = Link(permissions=[permission_document_type_view], text=_('Document types'), view='documents:document_type_list')
link_document_type_setup = Link(icon='fa fa-file', permissions=[permission_document_type_view], text=_('Document types'), view='documents:document_type_list')
link_document_type_create = Link(
permissions=[permission_document_type_create],
text=_('Create document type'), view='documents:document_type_create'
)
link_document_type_delete = Link(
permissions=[permission_document_type_delete], tags='dangerous',
text=_('Delete'), view='documents:document_type_delete',
args='resolved_object.id'
)
link_document_type_edit = Link(
permissions=[permission_document_type_edit], text=_('Edit'),
view='documents:document_type_edit', args='resolved_object.id'
)
link_document_type_filename_create = Link(
permissions=[permission_document_type_edit],
text=_('Add filename to document type'),
view='documents:document_type_filename_create', args='document_type.id'
)
link_document_type_filename_delete = Link(
permissions=[permission_document_type_edit], tags='dangerous',
text=_('Delete'), view='documents:document_type_filename_delete',
args='resolved_object.id'
)
link_document_type_filename_edit = Link(
permissions=[permission_document_type_edit], text=_('Edit'),
view='documents:document_type_filename_edit', args='resolved_object.id'
)
link_document_type_filename_list = Link(
permissions=[permission_document_type_view], text=_('Filenames'),
view='documents:document_type_filename_list', args='resolved_object.id'
)
link_document_type_list = Link(
permissions=[permission_document_type_view], text=_('Document types'),
view='documents:document_type_list'
)
link_document_type_setup = Link(
icon='fa fa-file', permissions=[permission_document_type_view],
text=_('Document types'), view='documents:document_type_list'
)

View File

@@ -13,7 +13,9 @@ logger = logging.getLogger(__name__)
class RecentDocumentManager(models.Manager):
def add_document_for_user(self, user, document):
if user.is_authenticated():
new_recent, created = self.model.objects.get_or_create(user=user, document=document)
new_recent, created = self.model.objects.get_or_create(
user=user, document=document
)
if not created:
# document already in the recent list, just save to force
# accessed date and time update
@@ -25,7 +27,9 @@ class RecentDocumentManager(models.Manager):
document_model = apps.get_model('documents', 'document')
if user.is_authenticated():
return document_model.objects.filter(recentdocument__user=user).order_by('-recentdocument__datetime_accessed')
return document_model.objects.filter(
recentdocument__user=user
).order_by('-recentdocument__datetime_accessed')
else:
return document_model.objects.none()
@@ -37,7 +41,9 @@ class DocumentTypeManager(models.Manager):
class DocumentManager(models.Manager):
def get_queryset(self):
return TrashCanQuerySet(self.model, using=self._db).filter(in_trash=False)
return TrashCanQuerySet(
self.model, using=self._db
).filter(in_trash=False)
def invalidate_cache(self):
for document in self.model.objects.all():
@@ -50,7 +56,9 @@ class PassthroughManager(models.Manager):
class TrashCanManager(models.Manager):
def get_queryset(self):
return super(TrashCanManager, self).get_queryset().filter(in_trash=True)
return super(
TrashCanManager, self
).get_queryset().filter(in_trash=True)
class TrashCanQuerySet(models.QuerySet):

View File

@@ -43,7 +43,8 @@ from .signals import (
post_document_created, post_document_type_change, post_version_upload
)
HASH_FUNCTION = lambda x: hashlib.sha256(x).hexdigest() # document image cache name hash function
# document image cache name hash function
HASH_FUNCTION = lambda x: hashlib.sha256(x).hexdigest()
logger = logging.getLogger(__name__)
@@ -57,11 +58,29 @@ class DocumentType(models.Model):
Define document types or classes to which a specific set of
properties can be attached
"""
label = models.CharField(max_length=32, unique=True, verbose_name=_('Label'))
trash_time_period = models.PositiveIntegerField(blank=True, help_text=_('Amount of time after which documents of this type will be moved to the trash.'), null=True, verbose_name=_('Trash time period'))
trash_time_unit = models.CharField(blank=True, choices=TIME_DELTA_UNIT_CHOICES, null=True, max_length=8, verbose_name=_('Trash time unit'))
delete_time_period = models.PositiveIntegerField(default=DEFAULT_DELETE_PERIOD, help_text=_('Amount of time after which documents of this type in the trash will be deleted.'), verbose_name=_('Delete time period'))
delete_time_unit = models.CharField(choices=TIME_DELTA_UNIT_CHOICES, default=DEFAULT_DELETE_TIME_UNIT, max_length=8, verbose_name=_('Delete time unit'))
label = models.CharField(
max_length=32, unique=True, verbose_name=_('Label')
)
trash_time_period = models.PositiveIntegerField(
blank=True, help_text=_(
'Amount of time after which documents of this type will be '
'moved to the trash.'
), null=True, verbose_name=_('Trash time period')
)
trash_time_unit = models.CharField(
blank=True, choices=TIME_DELTA_UNIT_CHOICES, null=True, max_length=8,
verbose_name=_('Trash time unit')
)
delete_time_period = models.PositiveIntegerField(
default=DEFAULT_DELETE_PERIOD, help_text=_(
'Amount of time after which documents of this type in the trash '
'will be deleted.'
), verbose_name=_('Delete time period')
)
delete_time_unit = models.CharField(
choices=TIME_DELTA_UNIT_CHOICES, default=DEFAULT_DELETE_TIME_UNIT,
max_length=8, verbose_name=_('Delete time unit')
)
objects = DocumentTypeManager()
@@ -77,13 +96,21 @@ class DocumentType(models.Model):
def new_document(self, file_object, label=None, description=None, language=None, _user=None):
try:
with transaction.atomic():
document = self.documents.create(description=description or '', label=label or unicode(file_object), language=language or setting_language.value)
document = self.documents.create(
description=description or '',
label=label or unicode(file_object),
language=language or setting_language.value
)
document.save(_user=_user)
document.new_version(file_object=file_object, _user=_user)
return document
except Exception as exception:
logger.critical('Unexpected exception while trying to create new document "%s" from document type "%s"; %s', label or unicode(file_object), self, exception)
logger.critical(
'Unexpected exception while trying to create new document '
'"%s" from document type "%s"; %s',
label or unicode(file_object), self, exception
)
raise
class Meta:
@@ -98,15 +125,38 @@ class Document(models.Model):
Defines a single document with it's fields and properties
"""
uuid = models.CharField(default=UUID_FUNCTION, editable=False, max_length=48)
document_type = models.ForeignKey(DocumentType, related_name='documents', verbose_name=_('Document type'))
label = models.CharField(blank=True, db_index=True, default='', max_length=255, help_text=_('The name of the document'), verbose_name=_('Label'))
description = models.TextField(blank=True, default='', verbose_name=_('Description'))
date_added = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_('Added'))
language = models.CharField(blank=True, choices=setting_language_choices.value, default=setting_language.value, max_length=8, verbose_name=_('Language'))
in_trash = models.BooleanField(default=False, editable=False, verbose_name=_('In trash?'))
deleted_date_time = models.DateTimeField(blank=True, editable=True, null=True, verbose_name=_('Date and time trashed'))
is_stub = models.BooleanField(default=True, editable=False, verbose_name=_('Is stub?'))
uuid = models.CharField(
default=UUID_FUNCTION, editable=False, max_length=48
)
document_type = models.ForeignKey(
DocumentType, related_name='documents',
verbose_name=_('Document type')
)
label = models.CharField(
blank=True, db_index=True, default='', max_length=255,
help_text=_('The name of the document'), verbose_name=_('Label')
)
description = models.TextField(
blank=True, default='', verbose_name=_('Description')
)
date_added = models.DateTimeField(
auto_now_add=True, db_index=True, verbose_name=_('Added')
)
language = models.CharField(
blank=True, choices=setting_language_choices.value,
default=setting_language.value, max_length=8,
verbose_name=_('Language')
)
in_trash = models.BooleanField(
default=False, editable=False, verbose_name=_('In trash?')
)
deleted_date_time = models.DateTimeField(
blank=True, editable=True, null=True,
verbose_name=_('Date and time trashed')
)
is_stub = models.BooleanField(
default=True, editable=False, verbose_name=_('Is stub?')
)
objects = DocumentManager()
passthrough = PassthroughManager()
@@ -123,7 +173,9 @@ class Document(models.Model):
self.document_type = document_type
self.save()
if has_changed or force:
post_document_type_change.send(sender=self.__class__, instance=self)
post_document_type_change.send(
sender=self.__class__, instance=self
)
def invalidate_cache(self):
for document_version in self.versions.all():
@@ -174,7 +226,9 @@ class Document(models.Model):
def new_version(self, file_object, comment=None, _user=None):
logger.info('Creating new document version for document: %s', self)
document_version = DocumentVersion(document=self, comment=comment or '', file=File(file_object))
document_version = DocumentVersion(
document=self, comment=comment or '', file=File(file_object)
)
document_version.save(_user=_user)
logger.info('New document version queued for document: %s', self)
@@ -238,7 +292,9 @@ class Document(models.Model):
# TODO: look to remove, only used by the OCR parser
def document_save_to_temp_dir(self, filename, buffer_size=1024 * 1024):
temporary_path = os.path.join(setting_temporary_directory.value, filename)
temporary_path = os.path.join(
setting_temporary_directory.value, filename
)
return self.save_to_file(temporary_path, buffer_size)
@@ -265,15 +321,30 @@ class DocumentVersion(models.Model):
def register_post_save_hook(cls, order, func):
cls._post_save_hooks[order] = func
document = models.ForeignKey(Document, related_name='versions', verbose_name=_('Document'))
timestamp = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_('Timestamp'))
comment = models.TextField(blank=True, default='', verbose_name=_('Comment'))
document = models.ForeignKey(
Document, related_name='versions', verbose_name=_('Document')
)
timestamp = models.DateTimeField(
auto_now_add=True, db_index=True, verbose_name=_('Timestamp')
)
comment = models.TextField(
blank=True, default='', verbose_name=_('Comment')
)
# File related fields
file = models.FileField(storage=storage_backend, upload_to=UUID_FUNCTION, verbose_name=_('File'))
mimetype = models.CharField(blank=True, editable=False, max_length=255, null=True)
encoding = models.CharField(blank=True, editable=False, max_length=64, null=True)
checksum = models.TextField(blank=True, editable=False, null=True, verbose_name=_('Checksum'))
file = models.FileField(
storage=storage_backend, upload_to=UUID_FUNCTION,
verbose_name=_('File')
)
mimetype = models.CharField(
blank=True, editable=False, max_length=255, null=True
)
encoding = models.CharField(
blank=True, editable=False, max_length=64, null=True
)
checksum = models.TextField(
blank=True, editable=False, null=True, verbose_name=_('Checksum')
)
class Meta:
verbose_name = _('Document version')
@@ -308,7 +379,10 @@ class DocumentVersion(models.Model):
self.save()
self.update_page_count(save=False)
logger.info('New document version "%s" created for document: %s', self, self.document)
logger.info(
'New document version "%s" created for document: %s',
self, self.document
)
self.document.is_stub = False
if not self.document.label:
@@ -316,15 +390,22 @@ class DocumentVersion(models.Model):
self.document.save()
except Exception as exception:
logger.error('Error creating new document version for document "%s"; %s', self.document, exception)
logger.error(
'Error creating new document version for document "%s"; %s',
self.document, exception
)
raise
else:
if new_document_version:
event_document_new_version.commit(actor=user, target=self.document)
event_document_new_version.commit(
actor=user, target=self.document
)
post_version_upload.send(sender=self.__class__, instance=self)
if tuple(self.document.versions.all()) == (self,):
post_document_created.send(sender=self.document.__class__, instance=self.document)
post_document_created.send(
sender=self.document.__class__, instance=self.document
)
def invalidate_cache(self):
cache_storage_backend.delete(self.cache_filename)
@@ -333,8 +414,8 @@ class DocumentVersion(models.Model):
def update_checksum(self, save=True):
"""
Open a document version's file and update the checksum field using the
user provided checksum function
Open a document version's file and update the checksum field using
the user provided checksum function
"""
if self.exists():
source = self.open()
@@ -346,7 +427,9 @@ class DocumentVersion(models.Model):
def update_page_count(self, save=True):
try:
with self.open() as file_object:
converter = converter_class(file_object=file_object, mime_type=self.mimetype)
converter = converter_class(
file_object=file_object, mime_type=self.mimetype
)
detected_pages = converter.get_page_count()
except UnknownFileFormat:
# If converter backend doesn't understand the format,
@@ -372,7 +455,10 @@ class DocumentVersion(models.Model):
"""
Delete the subsequent versions after this one
"""
logger.info('Reverting to document document: %s to version: %s', self.document, self)
logger.info(
'Reverting to document document: %s to version: %s',
self.document, self
)
event_document_version_revert.commit(actor=user, target=self.document)
@@ -381,13 +467,15 @@ class DocumentVersion(models.Model):
def update_mimetype(self, save=True):
"""
Read a document verions's file and determine the mimetype by calling the
get_mimetype wrapper
Read a document verions's file and determine the mimetype by calling
the get_mimetype wrapper
"""
if self.exists():
try:
with self.open() as file_object:
self.mimetype, self.encoding = get_mimetype(file_object=file_object)
self.mimetype, self.encoding = get_mimetype(
file_object=file_object
)
except:
self.mimetype = ''
self.encoding = ''
@@ -486,7 +574,10 @@ class DocumentVersion(models.Model):
return self.open()
except Exception as exception:
# Cleanup in case of error
logger.error('Error creating intermediate file "%s"; %s.', cache_filename, exception)
logger.error(
'Error creating intermediate file "%s"; %s.',
cache_filename, exception
)
cache_storage_backend.delete(cache_filename)
raise
@@ -497,8 +588,12 @@ class DocumentTypeFilename(models.Model):
List of filenames available to a specific document type for the
quick rename functionality
"""
document_type = models.ForeignKey(DocumentType, related_name='filenames', verbose_name=_('Document type'))
filename = models.CharField(db_index=True, max_length=128, verbose_name=_('Filename'))
document_type = models.ForeignKey(
DocumentType, related_name='filenames', verbose_name=_('Document type')
)
filename = models.CharField(
db_index=True, max_length=128, verbose_name=_('Filename')
)
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
def __str__(self):
@@ -516,11 +611,18 @@ class DocumentPage(models.Model):
"""
Model that describes a document version page
"""
document_version = models.ForeignKey(DocumentVersion, related_name='pages', verbose_name=_('Document version'))
page_number = models.PositiveIntegerField(db_index=True, default=1, editable=False, verbose_name=_('Page number'))
document_version = models.ForeignKey(
DocumentVersion, related_name='pages',
verbose_name=_('Document version')
)
page_number = models.PositiveIntegerField(
db_index=True, default=1, editable=False, verbose_name=_('Page number')
)
def __str__(self):
return _('Page %(page_num)d out of %(total_pages)d of %(document)s') % {
return _(
'Page %(page_num)d out of %(total_pages)d of %(document)s'
) % {
'document': unicode(self.document),
'page_num': self.page_number,
'total_pages': self.document_version.pages.count()
@@ -540,7 +642,9 @@ class DocumentPage(models.Model):
@property
def siblings(self):
return DocumentPage.objects.filter(document_version=self.document_version)
return DocumentPage.objects.filter(
document_version=self.document_version
)
# Compatibility methods
@property
@@ -582,14 +686,18 @@ class DocumentPage(models.Model):
if cache_storage_backend.exists(cache_filename):
logger.debug('Page cache file "%s" found', cache_filename)
converter = converter_class(file_object=cache_storage_backend.open(cache_filename))
converter = converter_class(
file_object=cache_storage_backend.open(cache_filename)
)
converter.seek(0)
else:
logger.debug('Page cache file "%s" not found', cache_filename)
try:
converter = converter_class(file_object=self.document_version.get_intermidiate_file())
converter = converter_class(
file_object=self.document_version.get_intermidiate_file()
)
converter.seek(page_number=self.page_number - 1)
page_image = converter.get_page()
@@ -598,7 +706,10 @@ class DocumentPage(models.Model):
file_object.write(page_image.getvalue())
except Exception as exception:
# Cleanup in case of error
logger.error('Error creating page cache file "%s"; %s', cache_filename, exception)
logger.error(
'Error creating page cache file "%s"; %s',
cache_filename, exception
)
cache_storage_backend.delete(cache_filename)
raise
@@ -611,13 +722,19 @@ class DocumentPage(models.Model):
converter.transform(transformation=transformation)
if rotation:
converter.transform(transformation=TransformationRotate(degrees=rotation))
converter.transform(transformation=TransformationRotate(
degrees=rotation)
)
if size:
converter.transform(transformation=TransformationResize(**dict(zip(('width', 'height'), (size.split('x'))))))
converter.transform(transformation=TransformationResize(
**dict(zip(('width', 'height'), (size.split('x')))))
)
if zoom_level:
converter.transform(transformation=TransformationZoom(percent=zoom_level))
converter.transform(
transformation=TransformationZoom(percent=zoom_level)
)
page_image = converter.get_page()
@@ -635,8 +752,12 @@ class RecentDocument(models.Model):
a given user
"""
user = models.ForeignKey(User, editable=False, verbose_name=_('User'))
document = models.ForeignKey(Document, editable=False, verbose_name=_('Document'))
datetime_accessed = models.DateTimeField(auto_now=True, db_index=True, verbose_name=_('Accessed'))
document = models.ForeignKey(
Document, editable=False, verbose_name=_('Document')
)
datetime_accessed = models.DateTimeField(
auto_now=True, db_index=True, verbose_name=_('Accessed')
)
objects = RecentDocumentManager()

View File

@@ -6,24 +6,61 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('documents', _('Documents'))
permission_document_create = namespace.add_permission(name='document_create', label=_('Create documents'))
permission_document_delete = namespace.add_permission(name='document_delete', label=_('Delete documents'))
permission_document_trash = namespace.add_permission(name='document_trash', label=_('Trash documents'))
permission_document_download = namespace.add_permission(name='document_download', label=_('Download documents'))
permission_document_edit = namespace.add_permission(name='document_edit', label=_('Edit documents'))
permission_document_new_version = namespace.add_permission(name='document_new_version', label=_('Create new document versions'))
permission_document_properties_edit = namespace.add_permission(name='document_properties_edit', label=_('Edit document properties'))
permission_document_print = namespace.add_permission(name='document_print', label=_('Can print documents'))
permission_document_restore = namespace.add_permission(name='document_restore', label=_('Restore deleted document'))
permission_document_tools = namespace.add_permission(name='document_tools', label=_('Execute document modifying tools'))
permission_document_version_revert = namespace.add_permission(name='document_version_revert', label=_('Revert documents to a previous version'))
permission_document_view = namespace.add_permission(name='document_view', label=_('View documents'))
permission_document_create = namespace.add_permission(
name='document_create', label=_('Create documents')
)
permission_document_delete = namespace.add_permission(
name='document_delete', label=_('Delete documents')
)
permission_document_trash = namespace.add_permission(
name='document_trash', label=_('Trash documents')
)
permission_document_download = namespace.add_permission(
name='document_download', label=_('Download documents')
)
permission_document_edit = namespace.add_permission(
name='document_edit', label=_('Edit documents')
)
permission_document_new_version = namespace.add_permission(
name='document_new_version', label=_('Create new document versions')
)
permission_document_properties_edit = namespace.add_permission(
name='document_properties_edit', label=_('Edit document properties')
)
permission_document_print = namespace.add_permission(
name='document_print', label=_('Can print documents')
)
permission_document_restore = namespace.add_permission(
name='document_restore', label=_('Restore deleted document')
)
permission_document_tools = namespace.add_permission(
name='document_tools', label=_('Execute document modifying tools')
)
permission_document_version_revert = namespace.add_permission(
name='document_version_revert',
label=_('Revert documents to a previous version')
)
permission_document_view = namespace.add_permission(
name='document_view', label=_('View documents')
)
permission_empty_trash = namespace.add_permission(name='document_empty_trash', label=_('Empty trash'))
permission_empty_trash = namespace.add_permission(
name='document_empty_trash', label=_('Empty trash')
)
setup_namespace = PermissionNamespace('documents_setup', label=_('Documents setup'))
setup_namespace = PermissionNamespace(
'documents_setup', label=_('Documents setup')
)
permission_document_type_create = setup_namespace.add_permission(name='document_type_create', label=_('Create document types'))
permission_document_type_delete = setup_namespace.add_permission(name='document_type_delete', label=_('Delete document types'))
permission_document_type_edit = setup_namespace.add_permission(name='document_type_edit', label=_('Edit document types'))
permission_document_type_view = setup_namespace.add_permission(name='document_type_view', label=_('View document types'))
permission_document_type_create = setup_namespace.add_permission(
name='document_type_create', label=_('Create document types')
)
permission_document_type_delete = setup_namespace.add_permission(
name='document_type_delete', label=_('Delete document types')
)
permission_document_type_edit = setup_namespace.add_permission(
name='document_type_edit', label=_('Edit document types')
)
permission_document_type_view = setup_namespace.add_permission(
name='document_type_view', label=_('View document types')
)

View File

@@ -6,9 +6,16 @@ from dynamic_search.classes import SearchModel
from .permissions import permission_document_view
document_search = SearchModel('documents', 'Document', permission=permission_document_view, serializer_string='documents.serializers.DocumentSerializer')
document_search = SearchModel(
'documents', 'Document', permission=permission_document_view,
serializer_string='documents.serializers.DocumentSerializer'
)
document_search.add_model_field(field='document_type__label', label=_('Document type'))
document_search.add_model_field(field='versions__mimetype', label=_('MIME type'))
document_search.add_model_field(
field='document_type__label', label=_('Document type')
)
document_search.add_model_field(
field='versions__mimetype', label=_('MIME type')
)
document_search.add_model_field(field='label', label=_('Label'))
document_search.add_model_field(field='description', label=_('Description'))

View File

@@ -40,11 +40,16 @@ class DocumentSerializer(serializers.ModelSerializer):
versions = DocumentVersionSerializer(many=True, read_only=True)
# TODO: Deprecate, move this as an entry point of DocumentVersion's pages
image = serializers.HyperlinkedIdentityField(view_name='document-image')
new_version = serializers.HyperlinkedIdentityField(view_name='document-new-version')
new_version = serializers.HyperlinkedIdentityField(
view_name='document-new-version'
)
document_type = DocumentTypeSerializer()
class Meta:
fields = ('id', 'label', 'image', 'new_version', 'uuid', 'document_type', 'description', 'date_added', 'versions')
fields = (
'id', 'label', 'image', 'new_version', 'uuid', 'document_type',
'description', 'date_added', 'versions'
)
model = Document
@@ -53,7 +58,10 @@ class NewDocumentSerializer(serializers.Serializer):
document_type = serializers.IntegerField()
file = serializers.FileField()
label = serializers.CharField(required=False)
language = serializers.ChoiceField(blank_display_value=None, choices=setting_language_choices.value, default=setting_language.value, required=False)
language = serializers.ChoiceField(
blank_display_value=None, choices=setting_language_choices.value,
default=setting_language.value, required=False
)
class RecentDocumentSerializer(serializers.ModelSerializer):

View File

@@ -7,21 +7,75 @@ from django.utils.translation import ugettext_lazy as _
from smart_settings import Namespace
# TODO: Findout method to make languages names' translatable.
# YAML fails to serialize ugettext_lazy and ugettext is not allowed at this level
LANGUAGE_CHOICES = [(i.terminology, i.name) for i in list(pycountry.languages)]
# YAML fails to serialize ugettext_lazy and ugettext is not allowed at this
# level
LANGUAGE_CHOICES = [
(i.terminology, i.name) for i in list(pycountry.languages)
]
namespace = Namespace(name='documents', label=_('Documents'))
setting_storage_backend = namespace.add_setting(global_name='DOCUMENTS_STORAGE_BACKEND', default='storage.backends.filebasedstorage.FileBasedStorage')
setting_preview_size = namespace.add_setting(global_name='DOCUMENTS_PREVIEW_SIZE', default='640x480')
setting_print_size = namespace.add_setting(global_name='DOCUMENTS_PRINT_SIZE', default='3600')
setting_multipage_preview_size = namespace.add_setting(global_name='DOCUMENTS_MULTIPAGE_PREVIEW_SIZE', default='160x120')
setting_thumbnail_size = namespace.add_setting(global_name='DOCUMENTS_THUMBNAIL_SIZE', default='50x50')
setting_display_size = namespace.add_setting(global_name='DOCUMENTS_DISPLAY_SIZE', default='3600')
setting_recent_count = namespace.add_setting(global_name='DOCUMENTS_RECENT_COUNT', default=40, help_text=_('Maximum number of recent (created, edited, viewed) documents to remember per user.'))
setting_zoom_percent_step = namespace.add_setting(global_name='DOCUMENTS_ZOOM_PERCENT_STEP', default=25, help_text=_('Amount in percent zoom in or out a document page per user interaction.'))
setting_zoom_max_level = namespace.add_setting(global_name='DOCUMENTS_ZOOM_MAX_LEVEL', default=300, help_text=_('Maximum amount in percent (%) to allow user to zoom in a document page interactively.'))
setting_zoom_min_level = namespace.add_setting(global_name='DOCUMENTS_ZOOM_MIN_LEVEL', default=25, help_text=_('Minimum amount in percent (%) to allow user to zoom out a document page interactively.'))
setting_rotation_step = namespace.add_setting(global_name='DOCUMENTS_ROTATION_STEP', default=90, help_text=_('Amount in degrees to rotate a document page per user interaction.'))
setting_cache_storage_backend = namespace.add_setting(global_name='DOCUMENTS_CACHE_STORAGE_BACKEND', default='documents.storage.LocalCacheFileStorage')
setting_language = namespace.add_setting(global_name='DOCUMENTS_LANGUAGE', default='eng', help_text=_('Default documents language (in ISO639-2 format).'))
setting_language_choices = namespace.add_setting(global_name='DOCUMENTS_LANGUAGE_CHOICES', default=LANGUAGE_CHOICES, help_text=_('List of supported document languages.'))
setting_storage_backend = namespace.add_setting(
global_name='DOCUMENTS_STORAGE_BACKEND',
default='storage.backends.filebasedstorage.FileBasedStorage'
)
setting_preview_size = namespace.add_setting(
global_name='DOCUMENTS_PREVIEW_SIZE', default='640x480'
)
setting_print_size = namespace.add_setting(
global_name='DOCUMENTS_PRINT_SIZE', default='3600'
)
setting_multipage_preview_size = namespace.add_setting(
global_name='DOCUMENTS_MULTIPAGE_PREVIEW_SIZE', default='160x120'
)
setting_thumbnail_size = namespace.add_setting(
global_name='DOCUMENTS_THUMBNAIL_SIZE', default='50x50'
)
setting_display_size = namespace.add_setting(
global_name='DOCUMENTS_DISPLAY_SIZE', default='3600'
)
setting_recent_count = namespace.add_setting(
global_name='DOCUMENTS_RECENT_COUNT', default=40,
help_text=_(
'Maximum number of recent (created, edited, viewed) documents to '
'remember per user.'
)
)
setting_zoom_percent_step = namespace.add_setting(
global_name='DOCUMENTS_ZOOM_PERCENT_STEP', default=25,
help_text=_(
'Amount in percent zoom in or out a document page per user '
'interaction.'
)
)
setting_zoom_max_level = namespace.add_setting(
global_name='DOCUMENTS_ZOOM_MAX_LEVEL', default=300,
help_text=_(
'Maximum amount in percent (%) to allow user to zoom in a document '
'page interactively.'
)
)
setting_zoom_min_level = namespace.add_setting(
global_name='DOCUMENTS_ZOOM_MIN_LEVEL', default=25,
help_text=_(
'Minimum amount in percent (%) to allow user to zoom out a document '
'page interactively.'
)
)
setting_rotation_step = namespace.add_setting(
global_name='DOCUMENTS_ROTATION_STEP', default=90,
help_text=_(
'Amount in degrees to rotate a document page per user interaction.'
)
)
setting_cache_storage_backend = namespace.add_setting(
global_name='DOCUMENTS_CACHE_STORAGE_BACKEND',
default='documents.storage.LocalCacheFileStorage'
)
setting_language = namespace.add_setting(
global_name='DOCUMENTS_LANGUAGE', default='eng',
help_text=_('Default documents language (in ISO639-2 format).')
)
setting_language_choices = namespace.add_setting(
global_name='DOCUMENTS_LANGUAGE_CHOICES', default=LANGUAGE_CHOICES,
help_text=_('List of supported document languages.')
)

View File

@@ -3,5 +3,7 @@ from __future__ import unicode_literals
from django.dispatch import Signal
post_version_upload = Signal(providing_args=['instance'], use_caching=True)
post_document_type_change = Signal(providing_args=['instance'], use_caching=True)
post_document_type_change = Signal(
providing_args=['instance'], use_caching=True
)
post_document_created = Signal(providing_args=['instance'], use_caching=True)

View File

@@ -14,7 +14,9 @@ def get_used_size(path, file_list):
total_size = 0
for filename in file_list:
try:
total_size += storage_backend.size(storage_backend.separator.join([path, filename]))
total_size += storage_backend.size(
storage_backend.separator.join([path, filename])
)
except OSError:
pass
@@ -45,11 +47,19 @@ class DocumentStatistics(Statistic):
results.extend([
_('Document types: %d') % DocumentType.objects.count(),
])
document_stats = DocumentVersion.objects.annotate(page_count=Count('pages')).aggregate(Min('page_count'), Max('page_count'), Avg('page_count'))
document_stats = DocumentVersion.objects.annotate(
page_count=Count('pages')
).aggregate(Min('page_count'), Max('page_count'), Avg('page_count'))
results.extend([
_('Minimum amount of pages per document: %d') % (document_stats['page_count__min'] or 0),
_('Maximum amount of pages per document: %d') % (document_stats['page_count__max'] or 0),
_('Average amount of pages per document: %f') % (document_stats['page_count__avg'] or 0),
_(
'Minimum amount of pages per document: %d'
) % (document_stats['page_count__min'] or 0),
_(
'Maximum amount of pages per document: %d'
) % (document_stats['page_count__max'] or 0),
_(
'Average amount of pages per document: %f'
) % (document_stats['page_count__avg'] or 0),
])
return results
@@ -69,16 +79,25 @@ class DocumentUsageStatistics(Statistic):
total_storage_documents, storage_used_space = storage_count()
results.append(_('Documents in storage: %d') %
total_storage_documents)
results.append(_('Space used in storage: %(base_2)s (base 2), %(base_10)s (base 10), %(bytes)d bytes') % {
results.append(
_(
'Space used in storage: %(base_2)s (base 2), %(base_10)s '
'(base 10), %(bytes)d bytes'
) % {
'base_2': pretty_size(storage_used_space),
'base_10': pretty_size_10(storage_used_space),
'bytes': storage_used_space
})
}
)
except NotImplementedError:
pass
results.extend([
_('Document pages in database: %d') % DocumentPage.objects.only('pk',).count(),
])
results.extend(
[
_(
'Document pages in database: %d'
) % DocumentPage.objects.only('pk',).count(),
]
)
return results

View File

@@ -27,16 +27,31 @@ def task_check_delete_periods():
logger.info('Executing')
for document_type in DocumentType.objects.all():
logger.info('Checking deletion period of document type: %s', document_type)
logger.info(
'Checking deletion period of document type: %s', document_type
)
if document_type.delete_time_period and document_type.delete_time_unit:
delta = timedelta(**{document_type.delete_time_unit: document_type.delete_time_period})
logger.info('Document type: %s, has a deletion period delta of: %s', document_type, delta)
delta = timedelta(
**{
document_type.delete_time_unit: document_type.delete_time_period
}
)
logger.info(
'Document type: %s, has a deletion period delta of: %s',
document_type, delta
)
for document in DeletedDocument.objects.filter(document_type=document_type):
if now() > document.deleted_date_time + delta:
logger.info('Document "%s" with id: %d, trashed on: %s, exceded delete period', document, document.pk, document.deleted_date_time)
logger.info(
'Document "%s" with id: %d, trashed on: %s, exceded '
'delete period', document, document.pk,
document.deleted_date_time
)
document.delete()
else:
logger.info('Document type: %s, has a no retention delta', document_type)
logger.info(
'Document type: %s, has a no retention delta', document_type
)
logger.info('Finshed')
@@ -46,16 +61,31 @@ def task_check_trash_periods():
logger.info('Executing')
for document_type in DocumentType.objects.all():
logger.info('Checking trash period of document type: %s', document_type)
logger.info(
'Checking trash period of document type: %s', document_type
)
if document_type.trash_time_period and document_type.trash_time_unit:
delta = timedelta(**{document_type.trash_time_unit: document_type.trash_time_period})
logger.info('Document type: %s, has a trash period delta of: %s', document_type, delta)
delta = timedelta(
**{
document_type.trash_time_unit: document_type.trash_time_period
}
)
logger.info(
'Document type: %s, has a trash period delta of: %s',
document_type, delta
)
for document in Document.objects.filter(document_type=document_type):
if now() > document.date_added + delta:
logger.info('Document "%s" with id: %d, added on: %s, exceded trash period', document, document.pk, document.date_added)
logger.info(
'Document "%s" with id: %d, added on: %s, exceded '
'trash period', document, document.pk,
document.date_added
)
document.delete()
else:
logger.info('Document type: %s, has a no retention delta', document_type)
logger.info(
'Document type: %s, has a no retention delta', document_type
)
logger.info('Finshed')
@@ -80,7 +110,11 @@ def task_update_page_count(self, version_id):
try:
document_version.update_page_count()
except OperationalError as exception:
logger.warning('Operational error during attempt to update page count for document version: %s; %s. Retrying.', document_version, exception)
logger.warning(
'Operational error during attempt to update page count for '
'document version: %s; %s. Retrying.', document_version,
exception
)
raise self.retry(exc=exception)
@@ -88,63 +122,100 @@ def task_update_page_count(self, version_id):
def task_upload_new_document(self, document_type_id, shared_uploaded_file_id, description=None, label=None, language=None, user_id=None):
try:
document_type = DocumentType.objects.get(pk=document_type_id)
shared_file = SharedUploadedFile.objects.get(pk=shared_uploaded_file_id)
shared_file = SharedUploadedFile.objects.get(
pk=shared_uploaded_file_id
)
if user_id:
user = User.objects.get(pk=user_id)
else:
user = None
except OperationalError as exception:
logger.warning('Operational error during attempt to gather data for new document: %s; Retrying.', exception)
logger.warning(
'Operational error during attempt to gather data for new '
'document: %s; Retrying.', exception
)
raise self.retry(exc=exception)
try:
with shared_file.open as file_object:
document_version = document_type.new_document(self, file_object=file_object, label=label, description=description, language=language, _user=user)
document_version = document_type.new_document(
self, file_object=file_object, label=label,
description=description, language=language, _user=user
)
except OperationalError as exception:
logger.warning('Operational error during attempt to gather data for new document: %s; Retrying.', exception)
logger.warning(
'Operational error during attempt to gather data for new '
'document: %s; Retrying.', exception
)
raise self.retry(exc=exception)
try:
shared_file.delete()
except OperationalError as exception:
logger.warning('Operational error while trying to delete shared file used to upload new document: %s; %s. Retrying.', document_version.document, exception)
logger.warning(
'Operational error while trying to delete shared file used to '
'upload new document: %s; %s. Retrying.',
document_version.document, exception
)
@app.task(bind=True, default_retry_delay=UPLOAD_NEW_VERSION_RETRY_DELAY, ignore_result=True)
def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, comment=None):
try:
document = Document.objects.get(pk=document_id)
shared_file = SharedUploadedFile.objects.get(pk=shared_uploaded_file_id)
shared_file = SharedUploadedFile.objects.get(
pk=shared_uploaded_file_id
)
if user_id:
user = User.objects.get(pk=user_id)
else:
user = None
except OperationalError as exception:
logger.warning('Operational error during attempt to retrieve shared data for new document version for:%s; %s. Retrying.', document, exception)
logger.warning(
'Operational error during attempt to retrieve shared data for '
'new document version for:%s; %s. Retrying.', document, exception
)
raise self.retry(exc=exception)
with shared_file.open() as file_object:
document_version = DocumentVersion(document=document, comment=comment, file=file_object)
document_version = DocumentVersion(
document=document, comment=comment, file=file_object
)
try:
document_version.save(_user=user)
except Warning as warning:
# New document version are blocked
logger.info('Warning during attempt to create new document version for document: %s; %s', document, warning)
logger.info(
'Warning during attempt to create new document version for '
'document: %s; %s', document, warning
)
shared_file.delete()
except OperationalError as exception:
logger.warning('Operational error during attempt to create new document version for document: %s; %s. Retrying.', document, exception)
logger.warning(
'Operational error during attempt to create new document '
'version for document: %s; %s. Retrying.', document, exception
)
raise self.retry(exc=exception)
except Exception as exception:
# This except and else block emulate a finally:
logger.error('Unexpected error during attempt to create new document version for document: %s; %s', document, exception)
logger.error(
'Unexpected error during attempt to create new document '
'version for document: %s; %s', document, exception
)
try:
shared_file.delete()
except OperationalError as exception:
logger.warning('Operational error during attempt to delete shared file: %s; %s.', shared_file, exception)
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)
else:
try:
shared_file.delete()
except OperationalError as exception:
logger.warning('Operational error during attempt to delete shared file: %s; %s.', shared_file, exception)
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)

View File

@@ -22,13 +22,18 @@ from .test_models import (
class DocumentAPICreateDocumentTestCase(TestCase):
"""
Functional test to make sure all the moving parts to create a document from
the API are working correctly
Functional test to make sure all the moving parts to create a document
from the API are working correctly
"""
def setUp(self):
self.admin_user = User.objects.create_superuser(username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD)
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
self.admin_user = User.objects.create_superuser(
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
password=TEST_ADMIN_PASSWORD
)
self.document_type = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE
)
ocr_settings = self.document_type.ocr_settings
ocr_settings.auto_ocr = False
@@ -41,7 +46,12 @@ class DocumentAPICreateDocumentTestCase(TestCase):
def test_uploading_a_document_using_token_auth(self):
# Get the an user token
token_client = APIClient()
response = token_client.post(reverse('auth_token_obtain'), {'username': TEST_ADMIN_USERNAME, 'password': TEST_ADMIN_PASSWORD})
response = token_client.post(
reverse('auth_token_obtain'), {
'username': TEST_ADMIN_USERNAME,
'password': TEST_ADMIN_PASSWORD
}
)
# Be able to get authentication token
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -66,17 +76,26 @@ class DocumentAPICreateDocumentTestCase(TestCase):
# Create a blank document
with open(TEST_SMALL_DOCUMENT_PATH) as file_descriptor:
document_response = document_client.post(reverse('document-list'), {'document_type': self.document_type.pk, 'file': file_descriptor})
document_response = document_client.post(
reverse('document-list'), {
'document_type': self.document_type.pk,
'file': file_descriptor
}
)
self.assertEqual(document_response.status_code, status.HTTP_201_CREATED)
# The document was created in the DB?
self.assertEqual(Document.objects.count(), 1)
new_version_url = reverse('document-new-version', args=[Document.objects.first().pk])
new_version_url = reverse(
'document-new-version', args=[Document.objects.first().pk]
)
with open(TEST_DOCUMENT_PATH) as file_descriptor:
response = document_client.post(new_version_url, {'file': file_descriptor})
response = document_client.post(
new_version_url, {'file': file_descriptor}
)
# Make sure the document uploaded correctly
document = Document.objects.first()
@@ -86,13 +105,20 @@ class DocumentAPICreateDocumentTestCase(TestCase):
self.assertEqual(document.file_mimetype, 'application/pdf')
self.assertEqual(document.file_mime_encoding, 'binary')
self.assertEqual(document.label, TEST_SMALL_DOCUMENT_FILENAME)
self.assertEqual(document.checksum, 'c637ffab6b8bb026ed3784afdb07663fddc60099853fae2be93890852a69ecf3')
self.assertEqual(
document.checksum,
'c637ffab6b8bb026ed3784afdb07663fddc60099853fae2be93890852a69ecf3'
)
self.assertEqual(document.page_count, 47)
# Make sure we can edit the document via the API
document_url = reverse('document-detail', args=[Document.objects.first().pk])
document_url = reverse(
'document-detail', args=[Document.objects.first().pk]
)
response = document_client.post(document_url, {'description': 'edited test document'})
response = document_client.post(
document_url, {'description': 'edited test document'}
)
# self.assertTrue(document.description, 'edited test document')

View File

@@ -17,26 +17,47 @@ TEST_COMPRESSED_DOCUMENTS_FILENAME = 'compressed_documents.zip'
TEST_SMALL_DOCUMENT_FILENAME = 'title_page.png'
TEST_NON_ASCII_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png'
TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png.zip'
TEST_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf')
TEST_SMALL_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_SMALL_DOCUMENT_FILENAME)
TEST_NON_ASCII_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_NON_ASCII_DOCUMENT_FILENAME)
TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME)
TEST_DEU_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_DEU_DOCUMENT_FILENAME)
TEST_COMPRESSED_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_COMPRESSED_DOCUMENTS_FILENAME)
TEST_DOCUMENT_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf'
)
TEST_SMALL_DOCUMENT_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents',
TEST_SMALL_DOCUMENT_FILENAME
)
TEST_NON_ASCII_DOCUMENT_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents',
TEST_NON_ASCII_DOCUMENT_FILENAME
)
TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents',
TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME
)
TEST_DEU_DOCUMENT_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents',
TEST_DEU_DOCUMENT_FILENAME
)
TEST_COMPRESSED_DOCUMENT_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents',
TEST_COMPRESSED_DOCUMENTS_FILENAME
)
TEST_DOCUMENT_DESCRIPTION = 'test description'
TEST_DOCUMENT_TYPE = 'test_document_type'
class DocumentTestCase(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()
with open(TEST_DOCUMENT_PATH) as file_object:
self.document = self.document_type.new_document(file_object=File(file_object), label='mayan_11_1.pdf')
self.document = self.document_type.new_document(
file_object=File(file_object), label='mayan_11_1.pdf'
)
def tearDown(self):
self.document_type.delete()
@@ -50,7 +71,10 @@ class DocumentTestCase(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_version_creation(self):
@@ -58,7 +82,9 @@ class DocumentTestCase(TestCase):
self.document.new_version(file_object=File(file_object))
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
self.document.new_version(file_object=File(file_object), comment='test comment 1')
self.document.new_version(
file_object=File(file_object), comment='test comment 1'
)
self.assertEqual(self.document.versions.count(), 3)

View File

@@ -22,20 +22,29 @@ class DocumentsViewsFunctionalTestCase(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()
# 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())
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
self.document = self.document_type.new_document(file_object=File(file_object), label='mayan_11_1.pdf')
self.document = self.document_type.new_document(
file_object=File(file_object), label='mayan_11_1.pdf'
)
def tearDown(self):
self.document_type.delete()
@@ -45,12 +54,16 @@ class DocumentsViewsFunctionalTestCase(TestCase):
self.assertEqual(Document.objects.count(), 1)
# Trash the document
self.client.post(reverse('documents:document_trash', args=[self.document.pk]))
self.client.post(
reverse('documents:document_trash', args=[self.document.pk])
)
self.assertEqual(DeletedDocument.objects.count(), 1)
self.assertEqual(Document.objects.count(), 0)
# Restore the document
self.client.post(reverse('documents:document_restore', args=[self.document.pk]))
self.client.post(
reverse('documents:document_restore', args=[self.document.pk])
)
self.assertEqual(DeletedDocument.objects.count(), 0)
self.assertEqual(Document.objects.count(), 1)
@@ -58,12 +71,16 @@ class DocumentsViewsFunctionalTestCase(TestCase):
self.assertEqual(Document.objects.count(), 1)
# Trash the document
self.client.post(reverse('documents:document_trash', args=[self.document.pk]))
self.client.post(
reverse('documents:document_trash', args=[self.document.pk])
)
self.assertEqual(DeletedDocument.objects.count(), 1)
self.assertEqual(Document.objects.count(), 0)
# Delete the document
self.client.post(reverse('documents:document_delete', args=[self.document.pk]))
self.client.post(
reverse('documents:document_delete', args=[self.document.pk])
)
self.assertEqual(DeletedDocument.objects.count(), 0)
self.assertEqual(Document.objects.count(), 0)
@@ -72,8 +89,12 @@ class DocumentsViewsFunctionalTestCase(TestCase):
self.assertContains(response, 'Total: 1', status_code=200)
# test document simple view
response = self.client.get(reverse('documents:document_properties', args=[self.document.pk]))
self.assertContains(response, 'roperties for document', status_code=200)
response = self.client.get(
reverse('documents:document_properties', args=[self.document.pk])
)
self.assertContains(
response, 'roperties for document', status_code=200
)
def test_document_type_views(self):
# Check that there are no document types
@@ -81,8 +102,12 @@ class DocumentsViewsFunctionalTestCase(TestCase):
self.assertContains(response, 'Total: 1', status_code=200)
# Create a document type
response = self.client.post(reverse('documents:document_type_create'), {'name': 'test document type 2'}, follow=True)
#TODO: FIX self.assertContains(response, 'successfully', status_code=200)
response = self.client.post(
reverse('documents:document_type_create'),
{'name': 'test document type 2'}, follow=True
)
#TODO: FIX
# self.assertContains(response, 'successfully', status_code=200)
# Check that there are two document types
response = self.client.get(reverse('documents:document_type_list'))
@@ -91,18 +116,42 @@ class DocumentsViewsFunctionalTestCase(TestCase):
self.assertEqual(self.document_type.label, TEST_DOCUMENT_TYPE)
# Edit the document type
response = self.client.post(reverse('documents:document_type_edit', args=[self.document_type.pk]), data={'name': TEST_DOCUMENT_TYPE + 'partial'}, follow=True)
#TODO: FIX self.assertContains(response, 'Document type edited successfully', status_code=200)
response = self.client.post(
reverse(
'documents:document_type_edit', args=[self.document_type.pk]
), data={'name': TEST_DOCUMENT_TYPE + 'partial'}, follow=True
)
#TODO: FIX
# self.assertContains(
# response, 'Document type edited successfully', status_code=200
#)
# Reload document type model data
#self.document_type = DocumentType.objects.get(pk=self.document_type.pk)
#TODO: FIX self.assertEqual(self.document_type.name, TEST_DOCUMENT_TYPE + 'partial')
#self.document_type = DocumentType.objects.get(
# pk=self.document_type.pk
#)
#TODO: FIX#
#self.assertEqual(
# self.document_type.name, TEST_DOCUMENT_TYPE + 'partial'
#)
# Delete the document type
response = self.client.post(reverse('documents:document_type_delete', args=[self.document_type.pk]), follow=True)
#TODO: FIX self.assertContains(response, 'Document type: {0} deleted successfully'.format(self.document_type.name), status_code=200)
#response = self.client.post(
# reverse(
# 'documents:document_type_delete', args=[self.document_type.pk]
# ), follow=True
#)
#TODO: FIX#
# self.assertContains(
# response, 'Document type: {0} deleted successfully'.format(
# self.document_type.name
# ), status_code=200
#)
# Check that there are no document types
response = self.client.get(reverse('documents:document_type_list'))
#response = self.client.get(reverse('documents:document_type_list'))
#TODO: FIX self.assertEqual(response.status_code, 200)
#TODO: FIX self.assertContains(response, 'ocument types (0)', status_code=200)
#TODO: FIX
#self.assertContains(
# response, 'ocument types (0)', status_code=200
#)

View File

@@ -21,74 +21,250 @@ from .views import (
urlpatterns = patterns(
'documents.views',
url(r'^list/$', DocumentListView.as_view(), name='document_list'),
url(r'^list/recent/$', RecentDocumentListView.as_view(), name='document_list_recent'),
url(r'^list/deleted/$', DeletedDocumentListView.as_view(), name='document_list_deleted'),
url(
r'^list/recent/$', RecentDocumentListView.as_view(),
name='document_list_recent'
),
url(
r'^list/deleted/$', DeletedDocumentListView.as_view(),
name='document_list_deleted'
),
url(r'^(?P<document_id>\d+)/preview/$', 'document_preview', name='document_preview'),
url(r'^(?P<document_id>\d+)/properties/$', 'document_properties', name='document_properties'),
url(r'^(?P<pk>\d+)/restore/$', DocumentRestoreView.as_view(), name='document_restore'),
url(r'^multiple/restore/$', DocumentManyRestoreView.as_view(), name='document_multiple_restore'),
url(r'^(?P<pk>\d+)/delete/$', DeletedDocumentDeleteView.as_view(), name='document_delete'),
url(r'^multiple/delete/$', DocumentManyDeleteView.as_view(), name='document_multiple_delete'),
url(r'^(?P<document_id>\d+)/type/$', 'document_document_type_edit', name='document_document_type_edit'),
url(r'^multiple/type/$', 'document_multiple_document_type_edit', name='document_multiple_document_type_edit'),
url(r'^(?P<document_id>\d+)/trash/$', 'document_trash', name='document_trash'),
url(r'^multiple/trash/$', 'document_multiple_trash', name='document_multiple_trash'),
url(r'^(?P<document_id>\d+)/edit/$', 'document_edit', name='document_edit'),
url(r'^(?P<document_id>\d+)/print/$', 'document_print', name='document_print'),
url(r'^(?P<document_id>\d+)/reset_page_count/$', 'document_update_page_count', name='document_update_page_count'),
url(r'^multiple/reset_page_count/$', 'document_multiple_update_page_count', name='document_multiple_update_page_count'),
url(
r'^(?P<document_id>\d+)/preview/$', 'document_preview',
name='document_preview'
),
url(
r'^(?P<document_id>\d+)/properties/$', 'document_properties',
name='document_properties'
),
url(
r'^(?P<pk>\d+)/restore/$', DocumentRestoreView.as_view(),
name='document_restore'
),
url(
r'^multiple/restore/$', DocumentManyRestoreView.as_view(),
name='document_multiple_restore'
),
url(
r'^(?P<pk>\d+)/delete/$', DeletedDocumentDeleteView.as_view(),
name='document_delete'
),
url(
r'^multiple/delete/$', DocumentManyDeleteView.as_view(),
name='document_multiple_delete'
),
url(
r'^(?P<document_id>\d+)/type/$', 'document_document_type_edit',
name='document_document_type_edit'
),
url(
r'^multiple/type/$', 'document_multiple_document_type_edit',
name='document_multiple_document_type_edit'
),
url(
r'^(?P<document_id>\d+)/trash/$', 'document_trash',
name='document_trash'
),
url(
r'^multiple/trash/$', 'document_multiple_trash',
name='document_multiple_trash'
),
url(
r'^(?P<document_id>\d+)/edit/$', 'document_edit',
name='document_edit'
),
url(
r'^(?P<document_id>\d+)/print/$', 'document_print',
name='document_print'
),
url(
r'^(?P<document_id>\d+)/reset_page_count/$',
'document_update_page_count', name='document_update_page_count'
),
url(
r'^multiple/reset_page_count/$',
'document_multiple_update_page_count',
name='document_multiple_update_page_count'
),
url(r'^(?P<document_id>\d+)/display/$', 'get_document_image', {'size': setting_display_size.value}, 'document_display'),
url(r'^(?P<document_id>\d+)/display/print/$', 'get_document_image', {'size': setting_print_size.value}, 'document_display_print'),
url(
r'^(?P<document_id>\d+)/display/$', 'get_document_image', {
'size': setting_display_size.value
}, 'document_display'
),
url(
r'^(?P<document_id>\d+)/display/print/$', 'get_document_image', {
'size': setting_print_size.value
}, 'document_display_print'
),
url(r'^(?P<document_id>\d+)/download/$', 'document_download', name='document_download'),
url(r'^multiple/download/$', 'document_multiple_download', name='document_multiple_download'),
url(r'^(?P<document_id>\d+)/clear_transformations/$', 'document_clear_transformations', name='document_clear_transformations'),
url(
r'^(?P<document_id>\d+)/download/$', 'document_download',
name='document_download'
),
url(
r'^multiple/download/$', 'document_multiple_download',
name='document_multiple_download'
),
url(
r'^(?P<document_id>\d+)/clear_transformations/$',
'document_clear_transformations',
name='document_clear_transformations'
),
url(r'^(?P<document_pk>\d+)/version/all/$', 'document_version_list', name='document_version_list'),
url(r'^document/version/(?P<document_version_pk>\d+)/download/$', 'document_download', name='document_version_download'),
url(r'^document/version/(?P<document_version_pk>\d+)/revert/$', 'document_version_revert', name='document_version_revert'),
url(
r'^(?P<document_pk>\d+)/version/all/$', 'document_version_list',
name='document_version_list'
),
url(
r'^document/version/(?P<document_version_pk>\d+)/download/$',
'document_download', name='document_version_download'
),
url(
r'^document/version/(?P<document_version_pk>\d+)/revert/$',
'document_version_revert', name='document_version_revert'
),
url(r'^(?P<pk>\d+)/pages/all/$', DocumentPageListView.as_view(), name='document_pages'),
url(
r'^(?P<pk>\d+)/pages/all/$', DocumentPageListView.as_view(),
name='document_pages'
),
url(r'^multiple/clear_transformations/$', 'document_multiple_clear_transformations', name='document_multiple_clear_transformations'),
url(r'^cache/clear/$', 'document_clear_image_cache', name='document_clear_image_cache'),
url(r'^trash_can/empty/$', EmptyTrashCanView.as_view(), name='trash_can_empty'),
url(
r'^multiple/clear_transformations/$',
'document_multiple_clear_transformations',
name='document_multiple_clear_transformations'
),
url(
r'^cache/clear/$', 'document_clear_image_cache',
name='document_clear_image_cache'
),
url(
r'^trash_can/empty/$', EmptyTrashCanView.as_view(),
name='trash_can_empty'
),
url(r'^page/(?P<document_page_id>\d+)/$', 'document_page_view', name='document_page_view'),
url(r'^page/(?P<document_page_id>\d+)/navigation/next/$', 'document_page_navigation_next', name='document_page_navigation_next'),
url(r'^page/(?P<document_page_id>\d+)/navigation/previous/$', 'document_page_navigation_previous', name='document_page_navigation_previous'),
url(r'^page/(?P<document_page_id>\d+)/navigation/first/$', 'document_page_navigation_first', name='document_page_navigation_first'),
url(r'^page/(?P<document_page_id>\d+)/navigation/last/$', 'document_page_navigation_last', name='document_page_navigation_last'),
url(r'^page/(?P<document_page_id>\d+)/zoom/in/$', 'document_page_zoom_in', name='document_page_zoom_in'),
url(r'^page/(?P<document_page_id>\d+)/zoom/out/$', 'document_page_zoom_out', name='document_page_zoom_out'),
url(r'^page/(?P<document_page_id>\d+)/rotate/right/$', 'document_page_rotate_right', name='document_page_rotate_right'),
url(r'^page/(?P<document_page_id>\d+)/rotate/left/$', 'document_page_rotate_left', name='document_page_rotate_left'),
url(r'^page/(?P<document_page_id>\d+)/reset/$', 'document_page_view_reset', name='document_page_view_reset'),
url(
r'^page/(?P<document_page_id>\d+)/$', 'document_page_view',
name='document_page_view'
),
url(
r'^page/(?P<document_page_id>\d+)/navigation/next/$',
'document_page_navigation_next', name='document_page_navigation_next'
),
url(
r'^page/(?P<document_page_id>\d+)/navigation/previous/$',
'document_page_navigation_previous',
name='document_page_navigation_previous'
),
url(
r'^page/(?P<document_page_id>\d+)/navigation/first/$',
'document_page_navigation_first', name='document_page_navigation_first'
),
url(
r'^page/(?P<document_page_id>\d+)/navigation/last/$',
'document_page_navigation_last', name='document_page_navigation_last'
),
url(
r'^page/(?P<document_page_id>\d+)/zoom/in/$',
'document_page_zoom_in', name='document_page_zoom_in'
),
url(
r'^page/(?P<document_page_id>\d+)/zoom/out/$',
'document_page_zoom_out', name='document_page_zoom_out'
),
url(
r'^page/(?P<document_page_id>\d+)/rotate/right/$',
'document_page_rotate_right', name='document_page_rotate_right'
),
url(
r'^page/(?P<document_page_id>\d+)/rotate/left/$',
'document_page_rotate_left', name='document_page_rotate_left'
),
url(
r'^page/(?P<document_page_id>\d+)/reset/$',
'document_page_view_reset', name='document_page_view_reset'
),
# Admin views
url(r'^type/list/$', DocumentTypeListView.as_view(), name='document_type_list'),
url(r'^type/create/$', DocumentTypeCreateView.as_view(), name='document_type_create'),
url(r'^type/(?P<pk>\d+)/edit/$', DocumentTypeEditView.as_view(), name='document_type_edit'),
url(r'^type/(?P<pk>\d+)/delete/$', DocumentTypeDeleteView.as_view(), name='document_type_delete'),
url(r'^type/(?P<pk>\d+)/documents/$', DocumentTypeDocumentListView.as_view(), name='document_type_document_list'),
url(r'^type/(?P<document_type_id>\d+)/filename/list/$', 'document_type_filename_list', name='document_type_filename_list'),
url(r'^type/filename/(?P<document_type_filename_id>\d+)/edit/$', 'document_type_filename_edit', name='document_type_filename_edit'),
url(r'^type/filename/(?P<document_type_filename_id>\d+)/delete/$', 'document_type_filename_delete', name='document_type_filename_delete'),
url(r'^type/(?P<document_type_id>\d+)/filename/create/$', 'document_type_filename_create', name='document_type_filename_create'),
url(
r'^type/list/$', DocumentTypeListView.as_view(),
name='document_type_list'
),
url(
r'^type/create/$', DocumentTypeCreateView.as_view(),
name='document_type_create'
),
url(
r'^type/(?P<pk>\d+)/edit/$', DocumentTypeEditView.as_view(),
name='document_type_edit'
),
url(
r'^type/(?P<pk>\d+)/delete/$', DocumentTypeDeleteView.as_view(),
name='document_type_delete'
),
url(
r'^type/(?P<pk>\d+)/documents/$',
DocumentTypeDocumentListView.as_view(),
name='document_type_document_list'
),
url(
r'^type/(?P<document_type_id>\d+)/filename/list/$',
'document_type_filename_list', name='document_type_filename_list'
),
url(
r'^type/filename/(?P<document_type_filename_id>\d+)/edit/$',
'document_type_filename_edit', name='document_type_filename_edit'
),
url(
r'^type/filename/(?P<document_type_filename_id>\d+)/delete/$',
'document_type_filename_delete', name='document_type_filename_delete'
),
url(
r'^type/(?P<document_type_id>\d+)/filename/create/$',
'document_type_filename_create', name='document_type_filename_create'
),
)
api_urls = patterns(
'',
url(r'^documents/$', APIDocumentListView.as_view(), name='document-list'),
url(r'^documents/recent/$', APIRecentDocumentListView.as_view(), name='document-recent-list'),
url(r'^documents/(?P<pk>[0-9]+)/$', APIDocumentView.as_view(), name='document-detail'),
url(r'^document_version/(?P<pk>[0-9]+)/$', APIDocumentVersionView.as_view(), name='documentversion-detail'),
url(r'^document_page/(?P<pk>[0-9]+)/$', APIDocumentPageView.as_view(), name='documentpage-detail'),
url(r'^documents/(?P<pk>[0-9]+)/image/$', APIDocumentImageView.as_view(), name='document-image'),
url(r'^documents/(?P<pk>[0-9]+)/new_version/$', APIDocumentVersionCreateView.as_view(), name='document-new-version'),
url(r'^documenttypes/(?P<pk>[0-9]+)/documents/$', APIDocumentTypeDocumentListView.as_view(), name='documenttype-document-list'),
url(r'^documenttypes/(?P<pk>[0-9]+)/$', APIDocumentTypeView.as_view(), name='documenttype-detail'),
url(r'^documenttypes/$', APIDocumentTypeListView.as_view(), name='documenttype-list'),
url(
r'^documents/recent/$', APIRecentDocumentListView.as_view(),
name='document-recent-list'
),
url(
r'^documents/(?P<pk>[0-9]+)/$', APIDocumentView.as_view(),
name='document-detail'
),
url(
r'^document_version/(?P<pk>[0-9]+)/$',
APIDocumentVersionView.as_view(), name='documentversion-detail'
),
url(
r'^document_page/(?P<pk>[0-9]+)/$', APIDocumentPageView.as_view(),
name='documentpage-detail'
),
url(
r'^documents/(?P<pk>[0-9]+)/image/$', APIDocumentImageView.as_view(),
name='document-image'
),
url(
r'^documents/(?P<pk>[0-9]+)/new_version/$',
APIDocumentVersionCreateView.as_view(), name='document-new-version'
),
url(
r'^documenttypes/(?P<pk>[0-9]+)/documents/$',
APIDocumentTypeDocumentListView.as_view(),
name='documenttype-document-list'
),
url(
r'^documenttypes/(?P<pk>[0-9]+)/$', APIDocumentTypeView.as_view(),
name='documenttype-detail'
),
url(
r'^documenttypes/$', APIDocumentTypeListView.as_view(),
name='documenttype-list'
),
)

View File

@@ -2,7 +2,8 @@ from __future__ import unicode_literals
def parse_range(astr):
# http://stackoverflow.com/questions/4248399/page-range-for-printing-algorithm
# http://stackoverflow.com/questions/4248399/
# page-range-for-printing-algorithm
result = set()
for part in astr.split(','):
x = part.split('-')

View File

@@ -41,8 +41,8 @@ from .forms import (
)
from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT
from .models import (
DeletedDocument, Document, DocumentType, DocumentPage, DocumentTypeFilename,
DocumentVersion, RecentDocument
DeletedDocument, Document, DocumentType, DocumentPage,
DocumentTypeFilename, DocumentVersion, RecentDocument
)
from .permissions import (
permission_document_delete, permission_document_download,
@@ -94,11 +94,17 @@ class DeletedDocumentListView(DocumentListView):
queryset = Document.trash.all()
try:
Permission.check_permissions(self.request.user, [permission_document_view])
Permission.check_permissions(
self.request.user, [permission_document_view]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_document_view, self.request.user, queryset)
AccessControlList.objects.check_access(
permission_document_view, self.request.user, queryset
)
return DeletedDocument.objects.filter(pk__in=queryset.values_list('pk', flat=True))
return DeletedDocument.objects.filter(
pk__in=queryset.values_list('pk', flat=True)
)
class DeletedDocumentDeleteView(ConfirmView):
@@ -107,12 +113,18 @@ class DeletedDocumentDeleteView(ConfirmView):
}
def object_action(self, request, instance):
source_document = get_object_or_404(Document.passthrough, pk=instance.pk)
source_document = get_object_or_404(
Document.passthrough, pk=instance.pk
)
try:
Permission.check_permissions(request.user, [permission_document_delete])
Permission.check_permissions(
request.user, [permission_document_delete]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_document_delete, request.user, source_document)
AccessControlList.objects.check_access(
permission_document_delete, request.user, source_document
)
instance.delete()
messages.success(request, _('Document: %(document)s deleted.') % {
@@ -132,16 +144,24 @@ class DocumentRestoreView(ConfirmView):
}
def object_action(self, request, instance):
source_document = get_object_or_404(Document.passthrough, pk=instance.pk)
source_document = get_object_or_404(
Document.passthrough, pk=instance.pk
)
try:
Permission.check_permissions(request.user, [permission_document_restore])
Permission.check_permissions(
request.user, [permission_document_restore]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_document_restore, request.user, source_document)
AccessControlList.objects.check_access(
permission_document_restore, request.user, source_document
)
instance.restore()
messages.success(request, _('Document: %(document)s restored.') % {
'document': instance}
messages.success(
request, _('Document: %(document)s restored.') % {
'document': instance
}
)
def post(self, request, *args, **kwargs):
@@ -204,7 +224,9 @@ class EmptyTrashCanView(ConfirmView):
'title': _('Empty trash?')
}
view_permission = permission_empty_trash
action_cancel_redirect = post_action_redirect = reverse_lazy('documents:document_list_deleted')
action_cancel_redirect = post_action_redirect = reverse_lazy(
'documents:document_list_deleted'
)
def post(self, request, *args, **kwargs):
for deleted_document in DeletedDocument.objects.all():
@@ -231,27 +253,43 @@ def document_properties(request, document_id):
try:
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
)
document.add_as_recent_document_for_user(request.user)
document_fields = [
{'label': _('Date added'), 'field': lambda x: x.date_added.date()},
{'label': _('Time added'), 'field': lambda x: unicode(x.date_added.time()).split('.')[0]},
{
'label': _('Time added'),
'field': lambda x: unicode(x.date_added.time()).split('.')[0]
},
{'label': _('UUID'), 'field': 'uuid'},
]
if document.latest_version:
document_fields.extend([
{'label': _('File mimetype'), 'field': lambda x: x.file_mimetype or _('None')},
{'label': _('File encoding'), 'field': lambda x: x.file_mime_encoding or _('None')},
{'label': _('File size'), 'field': lambda x: pretty_size(x.size) if x.size else '-'},
{
'label': _('File mimetype'),
'field': lambda x: x.file_mimetype or _('None')
},
{
'label': _('File encoding'),
'field': lambda x: x.file_mime_encoding or _('None')
},
{
'label': _('File size'),
'field': lambda x: pretty_size(x.size) if x.size else '-'
},
{'label': _('Exists in storage'), 'field': 'exists'},
{'label': _('File path in storage'), 'field': 'file'},
{'label': _('Checksum'), 'field': 'checksum'},
{'label': _('Pages'), 'field': 'page_count'},
])
document_properties_form = DocumentPropertiesForm(instance=document, extra_fields=document_fields)
document_properties_form = DocumentPropertiesForm(
instance=document, extra_fields=document_fields
)
return render_to_response('appearance/generic_form.html', {
'form': document_properties_form,
@@ -268,7 +306,9 @@ def document_preview(request, document_id):
try:
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
)
document.add_as_recent_document_for_user(request.user)
@@ -291,15 +331,23 @@ def document_trash(request, document_id=None, document_id_list=None):
documents = [get_object_or_404(Document, pk=document_id)]
post_action_redirect = reverse('documents:document_list_recent')
elif document_id_list:
documents = [get_object_or_404(Document, pk=document_id) for document_id in document_id_list.split(',')]
documents = [
get_object_or_404(Document, pk=document_id) for document_id in document_id_list.split(',')
]
else:
messages.error(request, _('Must provide at least one document.'))
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))
return HttpResponseRedirect(
request.META.get(
'HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)
)
)
try:
Permission.check_permissions(request.user, [permission_document_trash])
except PermissionDenied:
documents = AccessControlList.objects.filter_by_access(permission_document_trash, request.user, documents)
documents = AccessControlList.objects.filter_by_access(
permission_document_trash, request.user, documents
)
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))))

View File

@@ -21,8 +21,14 @@ class DocumentPageImageWidget(forms.widgets.Widget):
rotation = final_attrs.get('rotation', 0)
if value:
output = []
output.append('<div class="full-height scrollable mayan-page-wrapper-interactive" data-height-difference=230>')
output.append(document_html_widget(value, zoom=zoom, rotation=rotation, image_class='lazy-load', nolazyload=False, size=setting_display_size.value))
output.append(
'<div class="full-height scrollable '
'mayan-page-wrapper-interactive" data-height-difference=230>'
)
output.append(document_html_widget(
value, zoom=zoom, rotation=rotation, image_class='lazy-load',
nolazyload=False, size=setting_display_size.value)
)
output.append('</div>')
return mark_safe(''.join(output))
else:
@@ -35,7 +41,10 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget):
"""
def render(self, name, value, attrs=None):
output = []
output.append('<div id="carousel-container" class="full-height scrollable" data-height-difference=360>')
output.append(
'<div id="carousel-container" class="full-height scrollable" '
'data-height-difference=360>'
)
try:
document_pages = value.pages.all()
@@ -57,7 +66,14 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget):
post_load_class='lazy-load-carousel-loaded',
)
)
output.append('<div class="carousel-item-page-number">%s</div>' % ugettext('Page %(page_number)d of %(total_pages)d') % {'page_number': page.page_number, 'total_pages': total_pages})
output.append(
'<div class="carousel-item-page-number">%s</div>' % ugettext(
'Page %(page_number)d of %(total_pages)d'
) % {
'page_number': page.page_number,
'total_pages': total_pages
}
)
output.append('</div>')
output.append('</div>')
@@ -66,11 +82,16 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget):
def document_thumbnail(document, **kwargs):
return document_html_widget(document.latest_version.pages.first(), click_view='documents:document_display', **kwargs)
return document_html_widget(
document.latest_version.pages.first(),
click_view='documents:document_display', **kwargs
)
def document_link(document):
return mark_safe('<a href="%s">%s</a>' % (document.get_absolute_url(), document))
return mark_safe('<a href="%s">%s</a>' % (
document.get_absolute_url(), document)
)
def document_html_widget(document_page, click_view=None, click_view_arguments=None, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION, gallery_name=None, fancybox_class='fancybox', image_class='lazy-load', title=None, size=setting_thumbnail_size.value, nolazyload=False, post_load_class=None):
@@ -95,9 +116,15 @@ def document_html_widget(document_page, click_view=None, click_view_arguments=No
query_string = urlencode(query_dict)
preview_view = '%s?%s' % (reverse('document-image', args=[document.pk]), query_string)
preview_view = '%s?%s' % (
reverse('document-image', args=[document.pk]), query_string
)
result.append('<div class="tc" id="document-%d-%d">' % (document.pk, page if page else 1))
result.append(
'<div class="tc" id="document-%d-%d">' % (
document.pk, page if page else 1
)
)
if title:
title_template = 'title="%s"' % strip_tags(title)
@@ -105,17 +132,34 @@ def document_html_widget(document_page, click_view=None, click_view_arguments=No
title_template = ''
if click_view:
result.append('<a {gallery_template} class="{fancybox_class}" href="{image_data}" {title_template}>'.format(
result.append(
'<a {gallery_template} class="{fancybox_class}" '
'href="{image_data}" {title_template}>'.format(
gallery_template=gallery_template,
fancybox_class=fancybox_class,
image_data='%s?%s' % (reverse(click_view, args=click_view_arguments or [document.pk]), query_string),
image_data='%s?%s' % (
reverse(
click_view, args=click_view_arguments or [document.pk]
), query_string
),
title_template=title_template
))
)
)
if nolazyload:
result.append('<img class="img-nolazyload" src="%s" alt="%s" />' % (preview_view, alt_text))
result.append(
'<img class="img-nolazyload" src="%s" alt="%s" />' % (
preview_view, alt_text
)
)
else:
result.append('<img class="thin_border %s" data-src="%s" data-post-load-class="%s" src="%s" alt="%s" />' % (image_class, preview_view, post_load_class, static('appearance/images/loading.png'), alt_text))
result.append(
'<img class="thin_border %s" data-src="%s" '
'data-post-load-class="%s" src="%s" alt="%s" />' % (
image_class, preview_view, post_load_class,
static('appearance/images/loading.png'), alt_text
)
)
if click_view:
result.append('</a>')

View File

@@ -19,5 +19,12 @@ class DynamicSearchApp(MayanAppConfig):
APIEndPoint('search', app_name='dynamic_search')
menu_facet.bind_links(links=[link_search, link_search_advanced], sources=['search:search', 'search:search_advanced', 'search:results'])
menu_sidebar.bind_links(links=[link_search_again], sources=['search:results'])
menu_facet.bind_links(
links=[link_search, link_search_advanced],
sources=[
'search:search', 'search:search_advanced', 'search:results'
]
)
menu_sidebar.bind_links(
links=[link_search_again], sources=['search:results']
)

View File

@@ -72,13 +72,15 @@ class SearchModel(object):
findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
normspace=re.compile(r'\s{2,}').sub):
"""
Splits the query string in invidual keywords, getting rid of unecessary spaces
and grouping quoted words together.
Splits the query string in invidual keywords, getting rid of
unecessary spaces and grouping quoted words together.
Example:
>>> normalize_query(' some random words "with quotes " and spaces')
['some', 'random', 'words', 'with quotes', 'and', 'spaces']
"""
return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)]
return [
normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)
]
def search(self, query_string, user, global_and_search=False):
elapsed_time = 0
@@ -102,7 +104,9 @@ class SearchModel(object):
search_dict[search_field.get_model()]['searches'].append(
{
'field_name': [search_field.field],
'terms': self.normalize_query(query_string.get('q', '').strip())
'terms': self.normalize_query(
query_string.get('q', '').strip()
)
}
)
else:
@@ -117,7 +121,9 @@ class SearchModel(object):
search_dict[search_field.get_model()]['searches'].append(
{
'field_name': [search_field.field],
'terms': self.normalize_query(query_string[search_field.field])
'terms': self.normalize_query(
query_string[search_field.field]
)
}
)
@@ -129,7 +135,9 @@ class SearchModel(object):
for query_entry in data['searches']:
# Fashion a list of queries for a field for each term
field_query_list = self.assemble_query(query_entry['terms'], query_entry['field_name'])
field_query_list = self.assemble_query(
query_entry['terms'], query_entry['field_name']
)
logger.debug('field_query_list: %s', field_query_list)
@@ -139,20 +147,26 @@ class SearchModel(object):
# Get results per search field
for query in field_query_list:
logger.debug('query: %s', query)
term_query_result_set = set(model.objects.filter(query).values_list(data['return_value'], flat=True))
term_query_result_set = set(
model.objects.filter(query).values_list(
data['return_value'], flat=True
)
)
# Convert the QuerySet to a Python set and perform the
# AND operation on the program and not as a query.
# This operation ANDs all the field term results
# belonging to a single model, making sure to only include
# results in the final field result variable if all the terms
# are found in a single field.
# results in the final field result variable if all the
# terms are found in a single field.
if not field_result_set:
field_result_set = term_query_result_set
else:
field_result_set &= term_query_result_set
logger.debug('term_query_result_set: %s', term_query_result_set)
logger.debug(
'term_query_result_set: %s', term_query_result_set
)
logger.debug('field_result_set: %s', field_result_set)
if global_and_search:
@@ -165,24 +179,33 @@ class SearchModel(object):
result_set = result_set | model_result_set
elapsed_time = unicode(datetime.datetime.now() - start_time).split(':')[2]
elapsed_time = unicode(
datetime.datetime.now() - start_time
).split(':')[2]
queryset = self.model.objects.filter(pk__in=list(result_set)[:setting_limit.value])
queryset = self.model.objects.filter(
pk__in=list(result_set)[:setting_limit.value]
)
if self.permission:
try:
Permission.check_permissions(user, [self.permission])
except PermissionDenied:
queryset = AccessControlList.objects.filter_by_access(self.permission, user, queryset)
queryset = AccessControlList.objects.filter_by_access(
self.permission, user, queryset
)
RecentSearch.objects.add_query_for_user(user, query_string, len(result_set))
RecentSearch.objects.add_query_for_user(
user, query_string, len(result_set)
)
return queryset, result_set, elapsed_time
def assemble_query(self, terms, search_fields):
"""
Returns a query, that is a combination of Q objects. That combination
aims to search keywords within a model by testing the given search fields.
aims to search keywords within a model by testing the given search
fields.
"""
queries = []
for term in terms:

View File

@@ -5,5 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from navigation import Link
link_search = Link(text=_('Search'), view='search:search')
link_search_advanced = Link(text=_('Advanced search'), view='search:search_advanced')
link_search_advanced = Link(
text=_('Advanced search'), view='search:search_advanced'
)
link_search_again = Link(text=_('Search again'), view='search:search_again')

View File

@@ -9,7 +9,9 @@ from .settings import setting_recent_count
class RecentSearchManager(models.Manager):
def add_query_for_user(self, user, query_string, hits):
parsed_query = urlparse.parse_qs(urlencode(dict(query_string.items())))
parsed_query = urlparse.parse_qs(
urlencode(dict(query_string.items()))
)
for key, value in parsed_query.items():
parsed_query[key] = ' '.join(value)
@@ -26,7 +28,10 @@ class RecentSearchManager(models.Manager):
if parsed_query and not isinstance(user, AnonymousUser):
# If the URL query has at least one variable with a value
new_recent, created = self.model.objects.get_or_create(user=user, query=urlencode(parsed_query), defaults={'hits': hits})
new_recent, created = self.model.objects.get_or_create(
user=user, query=urlencode(parsed_query),
defaults={'hits': hits}
)
if not created:
new_recent.hits = hits
new_recent.save()

View File

@@ -6,7 +6,9 @@ import urlparse
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.encoding import python_2_unicode_compatible, smart_str, smart_unicode
from django.utils.encoding import (
python_2_unicode_compatible, smart_str, smart_unicode
)
from django.utils.translation import ugettext_lazy as _
from .managers import RecentSearchManager
@@ -24,13 +26,16 @@ class RecentSearch(models.Model):
# TODO: Fix after upgrade to DRF v2.4.4
query = models.TextField(editable=False, verbose_name=_('Query'))
datetime_created = models.DateTimeField(auto_now=True, db_index=True, verbose_name=_('Datetime created'))
datetime_created = models.DateTimeField(
auto_now=True, db_index=True, verbose_name=_('Datetime created')
)
hits = models.IntegerField(editable=False, verbose_name=_('Hits'))
objects = RecentSearchManager()
def __str__(self):
# TODO: Fix this hack, store the search model name in the recent search entry
# TODO: Fix this hack, store the search model name in the recent
# search entry
from .classes import SearchModel
document_search = SearchModel.get('documents.Document')
@@ -41,7 +46,11 @@ class RecentSearch(models.Model):
advanced_string = []
for key, value in query_dict.items():
search_field = document_search.get_search_field(key)
advanced_string.append('%s: %s' % (search_field.label, smart_unicode(' '.join(value))))
advanced_string.append(
'%s: %s' % (
search_field.label, smart_unicode(' '.join(value))
)
)
display_string = ', '.join(advanced_string)
else:

View File

@@ -6,6 +6,14 @@ from smart_settings import Namespace
namespace = Namespace(name='dynamic_search', label=_('Search'))
setting_show_object_type = namespace.add_setting(global_name='SEARCH_SHOW_OBJECT_TYPE', default=False)
setting_limit = namespace.add_setting(global_name='SEARCH_LIMIT', default=100, help_text=_('Maximum amount search hits to fetch and display.'))
setting_recent_count = namespace.add_setting(global_name='SEARCH_RECENT_COUNT', default=5, help_text=_('Maximum number of search queries to remember per user.'))
setting_show_object_type = namespace.add_setting(
global_name='SEARCH_SHOW_OBJECT_TYPE', default=False
)
setting_limit = namespace.add_setting(
global_name='SEARCH_LIMIT', default=100,
help_text=_('Maximum amount search hits to fetch and display.')
)
setting_recent_count = namespace.add_setting(
global_name='SEARCH_RECENT_COUNT', default=5,
help_text=_('Maximum number of search queries to remember per user.')
)

View File

@@ -14,11 +14,18 @@ from documents.test_models import (
class DocumentSearchTestCase(TestCase):
def setUp(self):
self.admin_user = User.objects.create_superuser(username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD)
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
self.admin_user = User.objects.create_superuser(
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
password=TEST_ADMIN_PASSWORD
)
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='mayan_11_1.pdf')
self.document = self.document_type.new_document(
file_object=File(file_object), label='mayan_11_1.pdf'
)
def tearDown(self):
self.document.delete()
@@ -30,17 +37,24 @@ class DocumentSearchTestCase(TestCase):
document versions and document version pages
"""
model_list, result_set, elapsed_time = document_search.search({'q': 'Mayan'}, user=self.admin_user)
model_list, result_set, elapsed_time = document_search.search(
{'q': 'Mayan'}, user=self.admin_user
)
self.assertEqual(len(result_set), 1)
self.assertEqual(list(model_list), [self.document])
def test_advanced_search_after_related_name_change(self):
# Test versions__filename
model_list, result_set, elapsed_time = document_search.search({'label': self.document.label}, user=self.admin_user)
model_list, result_set, elapsed_time = document_search.search(
{'label': self.document.label}, user=self.admin_user
)
self.assertEqual(len(result_set), 1)
self.assertEqual(list(model_list), [self.document])
# Test versions__mimetype
model_list, result_set, elapsed_time = document_search.search({'versions__mimetype': self.document.file_mimetype}, user=self.admin_user)
model_list, result_set, elapsed_time = document_search.search(
{'versions__mimetype': self.document.file_mimetype},
user=self.admin_user
)
self.assertEqual(len(result_set), 1)
self.assertEqual(list(model_list), [self.document])

View File

@@ -22,16 +22,23 @@ class Issue46TestCase(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()
# 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())
self.document_count = 4
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
self.document_type = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE
)
# Upload many instances of the same test document
for i in range(self.document_count):
@@ -49,16 +56,28 @@ class Issue46TestCase(TestCase):
def test_advanced_search_past_first_page(self):
# Make sure all documents are returned by the search
model_list, result_set, elapsed_time = document_search.search({'label': 'test document'}, user=self.admin_user)
model_list, result_set, elapsed_time = document_search.search(
{'label': 'test document'}, user=self.admin_user
)
self.assertEqual(len(result_set), self.document_count)
with self.settings(PAGINATION_DEFAULT_PAGINATION=2):
reload(pagination_tags)
# Funcitonal test for the first page of advanced results
response = self.client.get(reverse('search:results'), {'label': 'test'})
self.assertContains(response, 'Total (1 - 2 out of 4) (Page 1 of 2)', status_code=200)
response = self.client.get(
reverse('search:results'), {'label': 'test'}
)
self.assertContains(
response, 'Total (1 - 2 out of 4) (Page 1 of 2)',
status_code=200
)
# Functional test for the second page of advanced results
response = self.client.get(reverse('search:results'), {'label': 'test', 'page': 2})
self.assertContains(response, 'Total (3 - 4 out of 4) (Page 2 of 2)', status_code=200)
response = self.client.get(
reverse('search:results'), {'label': 'test', 'page': 2}
)
self.assertContains(
response, 'Total (3 - 4 out of 4) (Page 2 of 2)',
status_code=200
)

View File

@@ -16,7 +16,13 @@ urlpatterns = patterns(
api_urls = patterns(
'',
url(r'^recent_searches/$', APIRecentSearchListView.as_view(), name='recentsearch-list'),
url(r'^recent_searches/(?P<pk>[0-9]+)/$', APIRecentSearchView.as_view(), name='recentsearch-detail'),
url(
r'^recent_searches/$', APIRecentSearchListView.as_view(),
name='recentsearch-list'
),
url(
r'^recent_searches/(?P<pk>[0-9]+)/$', APIRecentSearchView.as_view(),
name='recentsearch-detail'
),
url(r'^search/$', APISearchView.as_view(), name='search-view'),
)

View File

@@ -44,18 +44,27 @@ def results(request, extra_context=None):
if setting_show_object_type.value:
context.update({
'extra_columns': [{'name': _('Type'), 'attribute': lambda x: x._meta.verbose_name[0].upper() + x._meta.verbose_name[1:]}]
'extra_columns': [
{
'name': _('Type'),
'attribute': lambda x: x._meta.verbose_name[0].upper() + x._meta.verbose_name[1:]
}
]
})
return render_to_response('dynamic_search/search_results.html', context,
context_instance=RequestContext(request))
return render_to_response(
'dynamic_search/search_results.html', context,
context_instance=RequestContext(request)
)
def search(request, advanced=False):
document_search = SearchModel.get('documents.Document')
if advanced:
form = AdvancedSearchForm(data=request.GET, search_model=document_search)
form = AdvancedSearchForm(
data=request.GET, search_model=document_search
)
return render_to_response(
'appearance/generic_form.html',
{
@@ -87,5 +96,9 @@ def search(request, advanced=False):
def search_again(request):
query = urlparse.urlparse(request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))).query
return HttpResponseRedirect('%s?%s' % (reverse('search:search_advanced'), query))
query = urlparse.urlparse(
request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))
).query
return HttpResponseRedirect(
'%s?%s' % (reverse('search:search_advanced'), query)
)

View File

@@ -21,6 +21,8 @@ class EventsApp(MayanAppConfig):
SourceColumn(source=Action, label=_('Timestamp'), attribute='timestamp')
SourceColumn(source=Action, label=_('Actor'), attribute='actor')
SourceColumn(source=Action, label=_('Verb'), attribute=encapsulate(lambda entry: event_type_link(entry)))
SourceColumn(source=Action, label=_('Verb'), attribute=encapsulate(
lambda entry: event_type_link(entry))
)
menu_tools.bind_links(links=[link_events_list])

View File

@@ -28,4 +28,7 @@ class Event(object):
if not self.event_type:
self.event_type, created = model.objects.get_or_create(name=self.name)
action.send(actor or target, actor=actor, verb=self.name, action_object=action_object, target=target)
action.send(
actor or target, actor=actor, verb=self.name,
action_object=action_object, target=target
)

View File

@@ -11,10 +11,21 @@ from .permissions import permission_events_view
def get_kwargs_factory(variable_name):
def get_kwargs(context):
content_type = ContentType.objects.get_for_model(context[variable_name])
return {'app_label': '"{}"'.format(content_type.app_label), 'model': '"{}"'.format(content_type.model), 'object_id': '{}.pk'.format(variable_name)}
return {
'app_label': '"{}"'.format(content_type.app_label),
'model': '"{}"'.format(content_type.model),
'object_id': '{}.pk'.format(variable_name)
}
return get_kwargs
link_events_list = Link(icon='fa fa-list-ol', permissions=[permission_events_view], text=_('Events'), view='events:events_list')
link_events_for_object = Link(permissions=[permission_events_view], text=_('Events'), view='events:events_for_object', kwargs=get_kwargs_factory('resolved_object'))
link_events_list = Link(
icon='fa fa-list-ol', permissions=[permission_events_view],
text=_('Events'), view='events:events_list'
)
link_events_for_object = Link(
permissions=[permission_events_view], text=_('Events'),
view='events:events_for_object',
kwargs=get_kwargs_factory('resolved_object')
)

View File

@@ -9,7 +9,9 @@ from .classes import Event
@python_2_unicode_compatible
class EventType(models.Model):
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'))
name = models.CharField(
max_length=64, unique=True, verbose_name=_('Name')
)
def __str__(self):
return unicode(Event.get_label(self.name))

View File

@@ -5,4 +5,6 @@ from django.utils.translation import ugettext_lazy as _
from permissions import PermissionNamespace
namespace = PermissionNamespace('events', _('Events'))
permission_events_view = namespace.add_permission(name='events_view', label=_('Access the events of an object'))
permission_events_view = namespace.add_permission(
name='events_view', label=_('Access the events of an object')
)

View File

@@ -7,6 +7,12 @@ from .views import EventListView, ObjectEventListView, VerbEventListView
urlpatterns = patterns(
'events.views',
url(r'^all/$', EventListView.as_view(), name='events_list'),
url(r'^for/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/$', ObjectEventListView.as_view(), name='events_for_object'),
url(r'^by_verb/(?P<verb>[\w\-]+)/$', VerbEventListView.as_view(), name='events_by_verb'),
url(
r'^for/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/$',
ObjectEventListView.as_view(), name='events_for_object'
),
url(
r'^by_verb/(?P<verb>[\w\-]+)/$', VerbEventListView.as_view(),
name='events_by_verb'
),
)

View File

@@ -29,7 +29,9 @@ class EventListView(SingleObjectListView):
'extra_columns': [
{
'name': _('Target'),
'attribute': encapsulate(lambda entry: event_object_link(entry))
'attribute': encapsulate(
lambda entry: event_object_link(entry)
)
}
],
'hide_object': True,
@@ -41,19 +43,30 @@ class ObjectEventListView(EventListView):
view_permissions = None
def dispatch(self, request, *args, **kwargs):
self.content_type = get_object_or_404(ContentType, app_label=self.kwargs['app_label'], model=self.kwargs['model'])
self.content_type = get_object_or_404(
ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
try:
self.content_object = self.content_type.get_object_for_this_type(pk=self.kwargs['object_id'])
self.content_object = self.content_type.get_object_for_this_type(
pk=self.kwargs['object_id']
)
except self.content_type.model_class().DoesNotExist:
raise Http404
try:
Permission.check_permissions(request.user, permissions=(permission_events_view,))
Permission.check_permissions(
request.user, permissions=(permission_events_view,)
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_events_view, request.user, self.content_object)
AccessControlList.objects.check_access(
permission_events_view, request.user, self.content_object
)
return super(ObjectEventListView, self).dispatch(request, *args, **kwargs)
return super(
ObjectEventListView, self
).dispatch(request, *args, **kwargs)
def get_queryset(self):
return any_stream(self.content_object)
@@ -75,9 +88,13 @@ class VerbEventListView(SingleObjectListView):
'extra_columns': [
{
'name': _('Target'),
'attribute': encapsulate(lambda entry: event_object_link(entry))
'attribute': encapsulate(
lambda entry: event_object_link(entry)
)
}
],
'hide_object': True,
'title': _('Events of type: %s') % Event.get_label(self.kwargs['verb']),
'title': _(
'Events of type: %s'
) % Event.get_label(self.kwargs['verb']),
}

View File

@@ -32,15 +32,21 @@ class APIFolderListView(generics.ListCreateAPIView):
mayan_view_permissions = {'POST': [permission_folder_create]}
def get(self, *args, **kwargs):
"""Returns a list of all the folders."""
"""
Returns a list of all the folders.
"""
return super(APIFolderListView, self).get(*args, **kwargs)
def post(self, *args, **kwargs):
"""Create a new folder."""
"""
Create a new folder.
"""
return super(APIFolderListView, 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():
serializer.object.user = request.user
@@ -48,8 +54,10 @@ class APIFolderListView(generics.ListCreateAPIView):
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
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)
@@ -67,24 +75,34 @@ class APIFolderView(generics.RetrieveUpdateDestroyAPIView):
}
def delete(self, *args, **kwargs):
"""Delete the selected folder."""
"""
Delete the selected folder.
"""
return super(APIFolderView, self).delete(*args, **kwargs)
def get(self, *args, **kwargs):
"""Returns the details of the selected folder."""
"""
Returns the details of the selected folder.
"""
return super(APIFolderView, self).get(*args, **kwargs)
def patch(self, *args, **kwargs):
"""Edit the selected folder."""
"""
Edit the selected folder.
"""
return super(APIFolderView, self).patch(*args, **kwargs)
def put(self, *args, **kwargs):
"""Edit the selected folder."""
"""
Edit the selected folder.
"""
return super(APIFolderView, self).put(*args, **kwargs)
class APIFolderDocumentListView(generics.ListAPIView):
"""Returns a list of all the documents contained in a particular folder."""
"""
Returns a list of all the documents contained in a particular folder.
"""
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {'GET': [permission_document_view]}
@@ -96,15 +114,21 @@ class APIFolderDocumentListView(generics.ListAPIView):
def get_queryset(self):
folder = get_object_or_404(Folder, pk=self.kwargs['pk'])
try:
Permission.check_permissions(self.request.user, [permission_folder_view])
Permission.check_permissions(
self.request.user, [permission_folder_view]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_folder_view, self.request.user, folder)
AccessControlList.objects.check_access(
permission_folder_view, self.request.user, folder
)
return folder.documents.all()
class APIDocumentFolderListView(generics.ListAPIView):
"""Returns a list of all the folders to which a document belongs."""
"""
Returns a list of all the folders to which a document belongs.
"""
serializer_class = FolderSerializer
@@ -114,9 +138,13 @@ class APIDocumentFolderListView(generics.ListAPIView):
def get_queryset(self):
document = get_object_or_404(Document, pk=self.kwargs['pk'])
try:
Permission.check_permissions(self.request.user, [permission_document_view])
Permission.check_permissions(
self.request.user, [permission_document_view]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_document_view, self.request.user, document)
AccessControlList.objects.check_access(
permission_document_view, self.request.user, document
)
queryset = document.folders.all()
return queryset
@@ -125,13 +153,19 @@ class APIDocumentFolderListView(generics.ListAPIView):
class APIFolderDocumentView(views.APIView):
def delete(self, request, *args, **kwargs):
"""Remove a document from the selected folder."""
"""
Remove a document from the selected folder.
"""
folder = get_object_or_404(Folder, pk=self.kwargs['pk'])
try:
Permission.check_permissions(request.user, [permission_folder_remove_document])
Permission.check_permissions(
request.user, [permission_folder_remove_document]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_folder_remove_document, request.user, folder)
AccessControlList.objects.check_access(
permission_folder_remove_document, request.user, folder
)
document = get_object_or_404(Document, pk=self.kwargs['document_pk'])
folder.documents.remove(document)
@@ -139,13 +173,19 @@ class APIFolderDocumentView(views.APIView):
# TODO: move this method as post of APIFolderDocumentListView
def post(self, request, *args, **kwargs):
"""Add a document to the selected folder."""
"""
Add a document to the selected folder.
"""
folder = get_object_or_404(Folder, pk=self.kwargs['pk'])
try:
Permission.check_permissions(request.user, [permission_folder_add_document])
Permission.check_permissions(
request.user, [permission_folder_add_document]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_folder_add_document, request.user, folder)
AccessControlList.objects.check_access(
permission_folder_add_document, request.user, folder
)
document = get_object_or_404(Document, pk=self.kwargs['document_pk'])
folder.documents.add(document)

View File

@@ -39,24 +39,48 @@ class FoldersApp(MayanAppConfig):
ModelPermission.register(
model=Document, permissions=(
permission_folder_add_document, permission_folder_remove_document
permission_folder_add_document,
permission_folder_remove_document
)
)
ModelPermission.register(
model=Folder, permissions=(
permission_acl_edit, permission_acl_view, permission_folder_delete,
permission_folder_edit, permission_folder_view
permission_acl_edit, permission_acl_view,
permission_folder_delete, permission_folder_edit,
permission_folder_view
)
)
menu_facet.bind_links(links=[link_document_folder_list], sources=[Document])
menu_facet.bind_links(
links=[link_document_folder_list], sources=[Document]
)
menu_main.bind_links(links=[link_folder_list])
menu_multi_item.bind_links(links=[link_folder_add_multiple_documents], sources=[Document])
menu_multi_item.bind_links(links=[link_folder_document_multiple_remove], sources=[CombinedSource(obj=Document, view='folders:folder_view')])
menu_object.bind_links(links=[link_folder_view, link_folder_edit, link_acl_list, link_folder_delete], sources=[Folder])
menu_secondary.bind_links(links=[link_folder_list, link_folder_create], sources=[Folder, 'folders:folder_list', 'folders:folder_create'])
menu_sidebar.bind_links(links=[link_folder_add_document], sources=['folders:document_folder_list', 'folders:folder_add_document'])
menu_multi_item.bind_links(
links=[link_folder_add_multiple_documents], sources=[Document]
)
menu_multi_item.bind_links(
links=[link_folder_document_multiple_remove],
sources=[CombinedSource(obj=Document, view='folders:folder_view')]
)
menu_object.bind_links(
links=[
link_folder_view, link_folder_edit, link_acl_list,
link_folder_delete
], sources=[Folder]
)
menu_secondary.bind_links(
links=[link_folder_list, link_folder_create],
sources=[Folder, 'folders:folder_list', 'folders:folder_create']
)
menu_sidebar.bind_links(
links=[link_folder_add_document],
sources=[
'folders:document_folder_list', 'folders:folder_add_document'
]
)
SourceColumn(source=Folder, label=_('Created'), attribute='datetime_created')
SourceColumn(
source=Folder, label=_('Created'), attribute='datetime_created'
)
SourceColumn(source=Folder, label=_('User'), attribute='user')

View File

@@ -25,7 +25,9 @@ class FolderListForm(forms.Form):
try:
Permission.check_permissions(user, [permission_folder_view])
except PermissionDenied:
queryset = AccessControlList.objects.filter_by_access(permission_folder_view, user, queryset)
queryset = AccessControlList.objects.filter_by_access(
permission_folder_view, user, queryset
)
self.fields['folder'] = forms.ModelChoiceField(
queryset=queryset,

View File

@@ -11,12 +11,38 @@ from .permissions import (
permission_folder_remove_document
)
link_document_folder_list = Link(permissions=[permission_document_view], text=_('Folders'), view='folders:document_folder_list', args='object.pk')
link_folder_add_document = Link(permissions=[permission_folder_add_document], text=_('Add to a folder'), view='folders:folder_add_document', args='object.pk')
link_folder_add_multiple_documents = Link(text=_('Add to folder'), view='folders:folder_add_multiple_documents')
link_folder_create = Link(permissions=[permission_folder_create], text=_('Create folder'), view='folders:folder_create')
link_folder_delete = Link(permissions=[permission_folder_delete], tags='dangerous', text=_('Delete'), view='folders:folder_delete', args='object.pk')
link_folder_document_multiple_remove = Link(permissions=[permission_folder_remove_document], text=_('Remove from folder'), view='folders:folder_document_multiple_remove', args='object.pk')
link_folder_edit = Link(permissions=[permission_folder_edit], text=_('Edit'), view='folders:folder_edit', args='object.pk')
link_folder_list = Link(icon='fa fa-folder', text=_('Folders'), view='folders:folder_list')
link_folder_view = Link(permissions=[permission_folder_view], text=_('Documents'), view='folders:folder_view', args='object.pk')
link_document_folder_list = Link(
permissions=[permission_document_view], text=_('Folders'),
view='folders:document_folder_list', args='object.pk'
)
link_folder_add_document = Link(
permissions=[permission_folder_add_document], text=_('Add to a folder'),
view='folders:folder_add_document', args='object.pk'
)
link_folder_add_multiple_documents = Link(
text=_('Add to folder'), view='folders:folder_add_multiple_documents'
)
link_folder_create = Link(
permissions=[permission_folder_create], text=_('Create folder'),
view='folders:folder_create'
)
link_folder_delete = Link(
permissions=[permission_folder_delete], tags='dangerous',
text=_('Delete'), view='folders:folder_delete', args='object.pk'
)
link_folder_document_multiple_remove = Link(
permissions=[permission_folder_remove_document],
text=_('Remove from folder'),
view='folders:folder_document_multiple_remove', args='object.pk'
)
link_folder_edit = Link(
permissions=[permission_folder_edit], text=_('Edit'),
view='folders:folder_edit', args='object.pk'
)
link_folder_list = Link(
icon='fa fa-folder', text=_('Folders'), view='folders:folder_list'
)
link_folder_view = Link(
permissions=[permission_folder_view], text=_('Documents'),
view='folders:folder_view', args='object.pk'
)

View File

@@ -11,10 +11,16 @@ from documents.models import Document
@python_2_unicode_compatible
class Folder(models.Model):
label = models.CharField(db_index=True, max_length=128, verbose_name=_('Label'))
label = models.CharField(
db_index=True, max_length=128, verbose_name=_('Label')
)
user = models.ForeignKey(User, verbose_name=_('User'))
datetime_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Datetime created'))
documents = models.ManyToManyField(Document, related_name='folders', verbose_name=_('Documents'))
datetime_created = models.DateTimeField(
auto_now_add=True, verbose_name=_('Datetime created')
)
documents = models.ManyToManyField(
Document, related_name='folders', verbose_name=_('Documents')
)
def __str__(self):
return self.label

View File

@@ -6,9 +6,21 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('folders', _('Folders'))
permission_folder_create = namespace.add_permission(name='folder_create', label=_('Create new folders'))
permission_folder_edit = namespace.add_permission(name='folder_edit', label=_('Edit new folders'))
permission_folder_delete = namespace.add_permission(name='folder_delete', label=_('Delete new folders'))
permission_folder_remove_document = namespace.add_permission(name='folder_remove_document', label=_('Remove documents from folders'))
permission_folder_view = namespace.add_permission(name='folder_view', label=_('View existing folders'))
permission_folder_add_document = namespace.add_permission(name='folder_add_document', label=_('Add documents to existing folders'))
permission_folder_create = namespace.add_permission(
name='folder_create', label=_('Create new folders')
)
permission_folder_edit = namespace.add_permission(
name='folder_edit', label=_('Edit new folders')
)
permission_folder_delete = namespace.add_permission(
name='folder_delete', label=_('Delete new folders')
)
permission_folder_remove_document = namespace.add_permission(
name='folder_remove_document', label=_('Remove documents from folders')
)
permission_folder_view = namespace.add_permission(
name='folder_view', label=_('View existing folders')
)
permission_folder_add_document = namespace.add_permission(
name='folder_add_document', label=_('Add documents to existing folders')
)

View File

@@ -15,17 +15,26 @@ from documents.test_models import TEST_DOCUMENT_TYPE
from .models import Folder
TEST_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'title_page.png')
TEST_DOCUMENT_PATH = os.path.join(
settings.BASE_DIR, 'contrib', 'sample_documents', 'title_page.png'
)
class FolderTestCase(TestCase):
def setUp(self):
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE)
self.document_type = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE
)
with open(TEST_DOCUMENT_PATH) as file_object:
self.document = self.document_type.new_document(file_object=File(file_object))
self.document = self.document_type.new_document(
file_object=File(file_object)
)
self.user = User.objects.create_superuser(username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD)
self.user = User.objects.create_superuser(
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
password=TEST_ADMIN_PASSWORD
)
def tearDown(self):
self.document.delete()

View File

@@ -16,20 +16,47 @@ urlpatterns = patterns(
url(r'^list/$', FolderListView.as_view(), name='folder_list'),
url(r'^create/$', FolderCreateView.as_view(), name='folder_create'),
url(r'^(?P<pk>\d+)/edit/$', FolderEditView.as_view(), name='folder_edit'),
url(r'^(?P<folder_id>\d+)/delete/$', 'folder_delete', name='folder_delete'),
url(
r'^(?P<folder_id>\d+)/delete/$', 'folder_delete', name='folder_delete'
),
url(r'^(?P<pk>\d+)/$', FolderDetailView.as_view(), name='folder_view'),
url(r'^(?P<folder_id>\d+)/remove/document/multiple/$', 'folder_document_multiple_remove', name='folder_document_multiple_remove'),
url(
r'^(?P<folder_id>\d+)/remove/document/multiple/$',
'folder_document_multiple_remove',
name='folder_document_multiple_remove'
),
url(r'^document/(?P<document_id>\d+)/folder/add/$', 'folder_add_document', name='folder_add_document'),
url(r'^document/multiple/folder/add/$', 'folder_add_multiple_documents', name='folder_add_multiple_documents'),
url(r'^document/(?P<pk>\d+)/folder/list/$', DocumentFolderListView.as_view(), name='document_folder_list'),
url(
r'^document/(?P<document_id>\d+)/folder/add/$',
'folder_add_document', name='folder_add_document'
),
url(
r'^document/multiple/folder/add/$', 'folder_add_multiple_documents',
name='folder_add_multiple_documents'
),
url(
r'^document/(?P<pk>\d+)/folder/list/$',
DocumentFolderListView.as_view(), name='document_folder_list'
),
)
api_urls = patterns(
'',
url(r'^folders/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$', APIFolderDocumentView.as_view(), name='folder-document'),
url(r'^folders/(?P<pk>[0-9]+)/documents/$', APIFolderDocumentListView.as_view(), name='folder-document-list'),
url(r'^folders/(?P<pk>[0-9]+)/$', APIFolderView.as_view(), name='folder-detail'),
url(
r'^folders/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$',
APIFolderDocumentView.as_view(), name='folder-document'
),
url(
r'^folders/(?P<pk>[0-9]+)/documents/$',
APIFolderDocumentListView.as_view(), name='folder-document-list'
),
url(
r'^folders/(?P<pk>[0-9]+)/$', APIFolderView.as_view(),
name='folder-detail'
),
url(r'^folders/$', APIFolderListView.as_view(), name='folder-list'),
url(r'^document/(?P<pk>[0-9]+)/folders/$', APIDocumentFolderListView.as_view(), name='document-folder-list'),
url(
r'^document/(?P<pk>[0-9]+)/folders/$',
APIDocumentFolderListView.as_view(), name='document-folder-list'
),
)

View File

@@ -53,7 +53,9 @@ class FolderListView(SingleObjectListView):
try:
Permission.check_permissions(user, [permission_document_view])
except PermissionDenied:
queryset = AccessControlList.objects.filter_by_access(permission_document_view, user, queryset)
queryset = AccessControlList.objects.filter_by_access(
permission_document_view, user, queryset
)
return queryset.count()
@@ -69,7 +71,14 @@ class FolderListView(SingleObjectListView):
def get_extra_context(self):
return {
'extra_columns': [
{'name': _('Documents'), 'attribute': encapsulate(lambda instance: FolderListView.get_document_count(instance=instance, user=self.request.user))},
{
'name': _('Documents'),
'attribute': encapsulate(
lambda instance: FolderListView.get_document_count(
instance=instance, user=self.request.user
)
)
},
],
'title': _('Folders'),
'hide_link': True,
@@ -83,14 +92,21 @@ class FolderCreateView(SingleObjectCreateView):
def form_valid(self, form):
try:
Folder.objects.get(label=form.cleaned_data['label'], user=self.request.user)
Folder.objects.get(
label=form.cleaned_data['label'], user=self.request.user
)
except Folder.DoesNotExist:
instance = form.save(commit=False)
instance.user = self.request.user
instance.save()
return super(FolderCreateView, self).form_valid(form)
else:
messages.error(self.request, _('A folder named: %s, already exists.') % form.cleaned_data['label'])
messages.error(
self.request,
_(
'A folder named: %s, already exists.'
) % form.cleaned_data['label']
)
return super(FolderCreateView, self).form_invalid(form)
def get_extra_context(self):
@@ -105,7 +121,9 @@ def folder_delete(request, folder_id):
try:
Permission.check_permissions(request.user, [permission_folder_delete])
except PermissionDenied:
AccessControlList.objects.check_access(permission_folder_delete, request.user, folder)
AccessControlList.objects.check_access(
permission_folder_delete, request.user, folder
)
post_action_redirect = reverse('folders:folder_list')
@@ -139,9 +157,13 @@ class FolderDetailView(DocumentListView):
folder = get_object_or_404(Folder, pk=self.kwargs['pk'])
try:
Permission.check_permissions(self.request.user, [permission_folder_view])
Permission.check_permissions(
self.request.user, [permission_folder_view]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_folder_view, self.request.user, folder)
AccessControlList.objects.check_access(
permission_folder_view, self.request.user, folder
)
return folder
@@ -216,9 +238,13 @@ class DocumentFolderListView(FolderListView):
self.document = get_object_or_404(Document, pk=self.kwargs['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, self.document)
AccessControlList.objects.check_access(
permission_document_view, request.user, self.document
)
return super(DocumentFolderListView, self).dispatch(request, *args, **kwargs)
@@ -228,7 +254,14 @@ class DocumentFolderListView(FolderListView):
def get_extra_context(self):
return {
'extra_columns': [
{'name': _('Documents'), 'attribute': encapsulate(lambda instance: FolderListView.get_document_count(instance=instance, user=self.request.user))},
{
'name': _('Documents'),
'attribute': encapsulate(
lambda instance: FolderListView.get_document_count(
instance=instance, user=self.request.user
)
)
},
],
'hide_link': True,
'object': self.document,

View File

@@ -7,7 +7,9 @@ from common.utils import encapsulate
from navigation import SourceColumn
from .classes import Property, PropertyNamespace, PIPNotFound, VirtualEnv
from .links import link_menu_link, link_namespace_details, link_namespace_list
from .links import (
link_menu_link, link_namespace_details, link_namespace_list
)
class InstallationApp(MayanAppConfig):
@@ -17,21 +19,35 @@ class InstallationApp(MayanAppConfig):
def ready(self):
super(InstallationApp, self).ready()
SourceColumn(source=PropertyNamespace, label=_('Label'), attribute='label')
SourceColumn(source=PropertyNamespace, label=_('Items'), attribute=encapsulate(lambda entry: len(entry.get_properties())))
SourceColumn(
source=PropertyNamespace, label=_('Label'), attribute='label'
)
SourceColumn(
source=PropertyNamespace, label=_('Items'),
attribute=encapsulate(lambda entry: len(entry.get_properties()))
)
SourceColumn(source=Property, label=_('Label'), attribute='label')
SourceColumn(source=Property, label=_('Value'), attribute='value')
menu_object.bind_links(links=[link_namespace_details], sources=[PropertyNamespace])
menu_secondary.bind_links(links=[link_namespace_list], sources=['installation:namespace_list', PropertyNamespace])
menu_object.bind_links(
links=[link_namespace_details], sources=[PropertyNamespace]
)
menu_secondary.bind_links(
links=[link_namespace_list],
sources=['installation:namespace_list', PropertyNamespace]
)
menu_tools.bind_links(links=[link_menu_link])
namespace = PropertyNamespace('venv', _('VirtualEnv'))
try:
venv = VirtualEnv()
except PIPNotFound:
namespace.add_property('pip', 'pip', _('pip not found.'), report=True)
namespace.add_property(
'pip', 'pip', _('pip not found.'), report=True
)
else:
for item, version, result in venv.get_results():
namespace.add_property(item, '%s (%s)' % (item, version), result, report=True)
namespace.add_property(
item, '%s (%s)' % (item, version), result, report=True
)

View File

@@ -108,7 +108,8 @@ class VirtualEnv(object):
# has no version number
return Dependency(string, version=None, standard=True)
else:
version = version.split('#')[0].split(' ')[1] # Get rid of '#egg' and '-e'
# Get rid of '#egg' and '-e'
version = version.split('#')[0].split(' ')[1]
return Dependency(package, version, standard=False)
else:
return Dependency(package, version, standard=True)
@@ -146,9 +147,12 @@ class VirtualEnv(object):
if item.version == installed_packages['%s-dev' % name.replace('-', '_')].version:
status = item.version
else:
status = installed_packages['%s-dev' % name.replace('-', '_')].version
status = installed_packages[
'%s-dev' % name.replace('-', '_')
].version
except KeyError:
# Not installed package found matching with name matchin requirement
# Not installed package found matching with name matching
# requirement
status = False
yield name, item.version, status

View File

@@ -6,6 +6,17 @@ from navigation import Link
from .permissions import permission_installation_details
link_menu_link = Link(icon='fa fa-check-square-o', permissions=[permission_installation_details], text=_('Installation details'), view='installation:namespace_list')
link_namespace_details = Link(permissions=[permission_installation_details], text=_('Details'), view='installation:namespace_details', args='object.id')
link_namespace_list = Link(permissions=[permission_installation_details], text=_('Installation property namespaces'), view='installation:namespace_list')
link_menu_link = Link(
icon='fa fa-check-square-o',
permissions=[permission_installation_details],
text=_('Installation details'), view='installation:namespace_list'
)
link_namespace_details = Link(
permissions=[permission_installation_details], text=_('Details'),
view='installation:namespace_details', args='object.id'
)
link_namespace_list = Link(
permissions=[permission_installation_details],
text=_('Installation property namespaces'),
view='installation:namespace_list'
)

View File

@@ -5,4 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from permissions import PermissionNamespace
namespace = PermissionNamespace('installation', _('Installation'))
permission_installation_details = namespace.add_permission(name='installation_details', label=_('View installation environment details'))
permission_installation_details = namespace.add_permission(
name='installation_details',
label=_('View installation environment details')
)

View File

@@ -5,5 +5,8 @@ from django.conf.urls import patterns, url
urlpatterns = patterns(
'installation.views',
url(r'^$', 'namespace_list', name='namespace_list'),
url(r'^(?P<namespace_id>\w+)/details/$', 'namespace_details', name='namespace_details'),
url(
r'^(?P<namespace_id>\w+)/details/$', 'namespace_details',
name='namespace_details'
),
)

View File

@@ -11,7 +11,9 @@ from .permissions import permission_installation_details
def namespace_list(request):
Permission.check_permissions(request.user, [permission_installation_details])
Permission.check_permissions(
request.user, [permission_installation_details]
)
return render_to_response('appearance/generic_list.html', {
'object_list': PropertyNamespace.get_all(),
@@ -21,7 +23,9 @@ def namespace_list(request):
def namespace_details(request, namespace_id):
Permission.check_permissions(request.user, [permission_installation_details])
Permission.check_permissions(
request.user, [permission_installation_details]
)
namespace = PropertyNamespace.get(namespace_id)
object_list = namespace.get_properties()

View File

@@ -41,10 +41,41 @@ class LinkingApp(MayanAppConfig):
)
)
menu_facet.bind_links(links=[link_smart_link_instances_for_document], sources=[Document])
menu_object.bind_links(links=[link_smart_link_condition_edit, link_smart_link_condition_delete], sources=[SmartLinkCondition])
menu_object.bind_links(links=[link_smart_link_edit, link_smart_link_document_types, link_smart_link_condition_list, link_acl_list, link_smart_link_delete], sources=[SmartLink])
menu_object.bind_links(links=[link_smart_link_instance_view], sources=[ResolvedSmartLink])
menu_secondary.bind_links(links=[link_smart_link_list, link_smart_link_create], sources=[SmartLink, 'linking:smart_link_list', 'linking:smart_link_create'])
menu_facet.bind_links(
links=[link_smart_link_instances_for_document],
sources=[Document]
)
menu_object.bind_links(
links=[
link_smart_link_condition_edit,
link_smart_link_condition_delete
], sources=[SmartLinkCondition]
)
menu_object.bind_links(
links=[
link_smart_link_edit, link_smart_link_document_types,
link_smart_link_condition_list, link_acl_list,
link_smart_link_delete
], sources=[SmartLink]
)
menu_object.bind_links(
links=[link_smart_link_instance_view],
sources=[ResolvedSmartLink]
)
menu_secondary.bind_links(
links=[link_smart_link_list, link_smart_link_create],
sources=[
SmartLink, 'linking:smart_link_list',
'linking:smart_link_create'
]
)
menu_setup.bind_links(links=[link_smart_link_setup])
menu_sidebar.bind_links(links=[link_smart_link_condition_create], sources=['linking:smart_link_condition_list', 'linking:smart_link_condition_create', 'linking:smart_link_condition_edit', 'linking:smart_link_condition_delete'])
menu_sidebar.bind_links(
links=[link_smart_link_condition_create],
sources=[
'linking:smart_link_condition_list',
'linking:smart_link_condition_create',
'linking:smart_link_condition_edit',
'linking:smart_link_condition_delete'
]
)

View File

@@ -12,7 +12,14 @@ from .models import SmartLink, SmartLinkCondition
class SmartLinkForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SmartLinkForm, self).__init__(*args, **kwargs)
self.fields['dynamic_label'].help_text = ' '.join([unicode(self.fields['dynamic_label'].help_text), ModelAttribute.help_text_for(Document, type_names=['field', 'related', 'property'])])
self.fields['dynamic_label'].help_text = ' '.join(
[
unicode(self.fields['dynamic_label'].help_text),
ModelAttribute.help_text_for(
Document, type_names=['field', 'related', 'property']
)
]
)
class Meta:
fields = ('label', 'dynamic_label', 'enabled')
@@ -22,8 +29,19 @@ class SmartLinkForm(forms.ModelForm):
class SmartLinkConditionForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SmartLinkConditionForm, self).__init__(*args, **kwargs)
self.fields['foreign_document_data'] = forms.ChoiceField(choices=ModelAttribute.get_choices_for(Document, type_names=['field', 'query']), label=_('Foreign document attribute'))
self.fields['expression'].help_text = ' '.join([unicode(self.fields['expression'].help_text), ModelAttribute.help_text_for(Document, type_names=['field', 'related', 'property'])])
self.fields['foreign_document_data'] = forms.ChoiceField(
choices=ModelAttribute.get_choices_for(
Document, type_names=['field', 'query']
), label=_('Foreign document attribute')
)
self.fields['expression'].help_text = ' '.join(
[
unicode(self.fields['expression'].help_text),
ModelAttribute.help_text_for(
Document, type_names=['field', 'related', 'property']
)
]
)
class Meta:
model = SmartLinkCondition

View File

@@ -10,15 +10,54 @@ from .permissions import (
permission_smart_link_edit, permission_smart_link_view
)
link_smart_link_condition_create = Link(permissions=[permission_smart_link_edit], text=_('Create condition'), view='linking:smart_link_condition_create', args='object.pk')
link_smart_link_condition_delete = Link(permissions=[permission_smart_link_edit], tags='dangerous', text=_('Delete'), view='linking:smart_link_condition_delete', args='resolved_object.pk')
link_smart_link_condition_edit = Link(permissions=[permission_smart_link_edit], text=_('Edit'), view='linking:smart_link_condition_edit', args='resolved_object.pk')
link_smart_link_condition_list = Link(permissions=[permission_smart_link_edit], text=_('Conditions'), view='linking:smart_link_condition_list', args='object.pk')
link_smart_link_create = Link(permissions=[permission_smart_link_create], text=_('Create new smart link'), view='linking:smart_link_create')
link_smart_link_delete = Link(permissions=[permission_smart_link_delete], tags='dangerous', text=_('Delete'), view='linking:smart_link_delete', args='object.pk')
link_smart_link_document_types = Link(permissions=[permission_smart_link_edit], text=_('Document types'), view='linking:smart_link_document_types', args='object.pk')
link_smart_link_edit = Link(permissions=[permission_smart_link_edit], text=_('Edit'), view='linking:smart_link_edit', args='object.pk')
link_smart_link_instance_view = Link(permissions=[permission_smart_link_view], text=_('Documents'), view='linking:smart_link_instance_view', args=['document.pk', 'object.pk'])
link_smart_link_instances_for_document = Link(permissions=[permission_document_view], text=_('Smart links'), view='linking:smart_link_instances_for_document', args='object.pk')
link_smart_link_list = Link(permissions=[permission_smart_link_create], text=_('Smart links'), view='linking:smart_link_list')
link_smart_link_setup = Link(icon='fa fa-link', permissions=[permission_smart_link_create], text=_('Smart links'), view='linking:smart_link_list')
link_smart_link_condition_create = Link(
permissions=[permission_smart_link_edit], text=_('Create condition'),
view='linking:smart_link_condition_create', args='object.pk'
)
link_smart_link_condition_delete = Link(
permissions=[permission_smart_link_edit], tags='dangerous',
text=_('Delete'), view='linking:smart_link_condition_delete',
args='resolved_object.pk'
)
link_smart_link_condition_edit = Link(
permissions=[permission_smart_link_edit], text=_('Edit'),
view='linking:smart_link_condition_edit', args='resolved_object.pk'
)
link_smart_link_condition_list = Link(
permissions=[permission_smart_link_edit], text=_('Conditions'),
view='linking:smart_link_condition_list', args='object.pk'
)
link_smart_link_create = Link(
permissions=[permission_smart_link_create],
text=_('Create new smart link'), view='linking:smart_link_create'
)
link_smart_link_delete = Link(
permissions=[permission_smart_link_delete], tags='dangerous',
text=_('Delete'), view='linking:smart_link_delete', args='object.pk'
)
link_smart_link_document_types = Link(
permissions=[permission_smart_link_edit], text=_('Document types'),
view='linking:smart_link_document_types', args='object.pk'
)
link_smart_link_edit = Link(
permissions=[permission_smart_link_edit], text=_('Edit'),
view='linking:smart_link_edit', args='object.pk'
)
link_smart_link_instance_view = Link(
permissions=[permission_smart_link_view], text=_('Documents'),
view='linking:smart_link_instance_view', args=[
'document.pk', 'object.pk'
]
)
link_smart_link_instances_for_document = Link(
permissions=[permission_document_view], text=_('Smart links'),
view='linking:smart_link_instances_for_document', args='object.pk'
)
link_smart_link_list = Link(
permissions=[permission_smart_link_create], text=_('Smart links'),
view='linking:smart_link_list'
)
link_smart_link_setup = Link(
icon='fa fa-link', permissions=[permission_smart_link_create],
text=_('Smart links'), view='linking:smart_link_list'
)

View File

@@ -15,9 +15,16 @@ from .literals import (
@python_2_unicode_compatible
class SmartLink(models.Model):
label = models.CharField(max_length=96, verbose_name=_('Label'))
dynamic_label = models.CharField(blank=True, max_length=96, help_text=_('This expression will be evaluated against the current selected document.'), verbose_name=_('Dynamic label'))
dynamic_label = models.CharField(
blank=True, max_length=96, help_text=_(
'This expression will be evaluated against the current selected '
'document.'
), verbose_name=_('Dynamic label')
)
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
document_types = models.ManyToManyField(DocumentType, verbose_name=_('Document types'))
document_types = models.ManyToManyField(
DocumentType, verbose_name=_('Document types')
)
def __str__(self):
return self.label
@@ -27,19 +34,30 @@ class SmartLink(models.Model):
try:
return eval(self.dynamic_label, {'document': document})
except Exception as exception:
return Exception(_('Error generating dynamic label; %s' % unicode(exception)))
return Exception(
_(
'Error generating dynamic label; %s' % unicode(exception)
)
)
else:
return self.label
def get_linked_document_for(self, document):
if document.document_type.pk not in self.document_types.values_list('pk', flat=True):
raise Exception(_('This smart link is not allowed for the selected document\'s type.'))
raise Exception(
_(
'This smart link is not allowed for the selected '
'document\'s type.'
)
)
smart_link_query = Q()
for condition in self.conditions.filter(enabled=True):
condition_query = Q(**{
'%s__%s' % (condition.foreign_document_data, condition.operator): eval(condition.expression, {'document': document})
'%s__%s' % (
condition.foreign_document_data, condition.operator
): eval(condition.expression, {'document': document})
})
if condition.negated:
condition_query = ~condition_query
@@ -55,7 +73,9 @@ class SmartLink(models.Model):
return Document.objects.none()
def resolve_for(self, document):
return ResolvedSmartLink(smart_link=self, queryset=self.get_linked_document_for(document))
return ResolvedSmartLink(
smart_link=self, queryset=self.get_linked_document_for(document)
)
class Meta:
verbose_name = _('Smart link')
@@ -69,16 +89,36 @@ class ResolvedSmartLink(SmartLink):
@python_2_unicode_compatible
class SmartLinkCondition(models.Model):
smart_link = models.ForeignKey(SmartLink, related_name='conditions', verbose_name=_('Smart link'))
inclusion = models.CharField(choices=INCLUSION_CHOICES, default=INCLUSION_AND, help_text=_('The inclusion is ignored for the first item.'), max_length=16)
foreign_document_data = models.CharField(help_text=_('This represents the metadata of all other documents.'), max_length=128, verbose_name=_('Foreign document attribute'))
smart_link = models.ForeignKey(
SmartLink, related_name='conditions', verbose_name=_('Smart link')
)
inclusion = models.CharField(
choices=INCLUSION_CHOICES, default=INCLUSION_AND,
help_text=_('The inclusion is ignored for the first item.'),
max_length=16
)
foreign_document_data = models.CharField(
help_text=_('This represents the metadata of all other documents.'),
max_length=128, verbose_name=_('Foreign document attribute')
)
operator = models.CharField(choices=OPERATOR_CHOICES, max_length=16)
expression = models.TextField(help_text=_('This expression will be evaluated against the current document.'), verbose_name=_('Expression'))
negated = models.BooleanField(default=False, help_text=_('Inverts the logic of the operator.'), verbose_name=_('Negated'))
expression = models.TextField(
help_text=_(
'This expression will be evaluated against the current document.'
), verbose_name=_('Expression')
)
negated = models.BooleanField(
default=False, help_text=_('Inverts the logic of the operator.'),
verbose_name=_('Negated')
)
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
def __str__(self):
return '%s foreign %s %s %s %s' % (self.get_inclusion_display(), self.foreign_document_data, _('not') if self.negated else '', self.get_operator_display(), self.expression)
return '%s foreign %s %s %s %s' % (
self.get_inclusion_display(),
self.foreign_document_data, _('not') if self.negated else '',
self.get_operator_display(), self.expression
)
class Meta:
verbose_name = _('Link condition')

View File

@@ -6,7 +6,15 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('linking', _('Smart links'))
permission_smart_link_view = namespace.add_permission(name='smart_link_view', label=_('View existing smart links'))
permission_smart_link_create = namespace.add_permission(name='smart_link_create', label=_('Create new smart links'))
permission_smart_link_delete = namespace.add_permission(name='smart_link_delete', label=_('Delete smart links'))
permission_smart_link_edit = namespace.add_permission(name='smart_link_edit', label=_('Edit smart links'))
permission_smart_link_view = namespace.add_permission(
name='smart_link_view', label=_('View existing smart links')
)
permission_smart_link_create = namespace.add_permission(
name='smart_link_create', label=_('Create new smart links')
)
permission_smart_link_delete = namespace.add_permission(
name='smart_link_delete', label=_('Delete smart links')
)
permission_smart_link_edit = namespace.add_permission(
name='smart_link_edit', label=_('Edit smart links')
)

View File

@@ -9,17 +9,47 @@ from .views import (
urlpatterns = patterns(
'linking.views',
url(r'^document/(?P<pk>\d+)/list/$', DocumentSmartLinkListView.as_view(), name='smart_link_instances_for_document'),
url(r'^document/(?P<document_pk>\d+)/(?P<smart_link_pk>\d+)/$', ResolvedSmartLinkView.as_view(), name='smart_link_instance_view'),
url(
r'^document/(?P<pk>\d+)/list/$', DocumentSmartLinkListView.as_view(),
name='smart_link_instances_for_document'
),
url(
r'^document/(?P<document_pk>\d+)/(?P<smart_link_pk>\d+)/$',
ResolvedSmartLinkView.as_view(), name='smart_link_instance_view'
),
url(r'^setup/list/$', SmartLinkListView.as_view(), name='smart_link_list'),
url(
r'^setup/list/$', SmartLinkListView.as_view(), name='smart_link_list'
),
url(r'^setup/create/$', 'smart_link_create', name='smart_link_create'),
url(r'^setup/(?P<smart_link_pk>\d+)/delete/$', 'smart_link_delete', name='smart_link_delete'),
url(r'^setup/(?P<smart_link_pk>\d+)/edit/$', 'smart_link_edit', name='smart_link_edit'),
url(r'^setup/(?P<pk>\d+)/document_types/$', SetupSmartLinkDocumentTypesView.as_view(), name='smart_link_document_types'),
url(
r'^setup/(?P<smart_link_pk>\d+)/delete/$', 'smart_link_delete',
name='smart_link_delete'
),
url(
r'^setup/(?P<smart_link_pk>\d+)/edit/$', 'smart_link_edit',
name='smart_link_edit'
),
url(
r'^setup/(?P<pk>\d+)/document_types/$',
SetupSmartLinkDocumentTypesView.as_view(),
name='smart_link_document_types'
),
url(r'^setup/(?P<smart_link_pk>\d+)/condition/list/$', 'smart_link_condition_list', name='smart_link_condition_list'),
url(r'^setup/(?P<smart_link_pk>\d+)/condition/create/$', 'smart_link_condition_create', name='smart_link_condition_create'),
url(r'^setup/smart_link/condition/(?P<smart_link_condition_pk>\d+)/edit/$', 'smart_link_condition_edit', name='smart_link_condition_edit'),
url(r'^setup/smart_link/condition/(?P<smart_link_condition_pk>\d+)/delete/$', 'smart_link_condition_delete', name='smart_link_condition_delete'),
url(
r'^setup/(?P<smart_link_pk>\d+)/condition/list/$',
'smart_link_condition_list', name='smart_link_condition_list'
),
url(
r'^setup/(?P<smart_link_pk>\d+)/condition/create/$',
'smart_link_condition_create', name='smart_link_condition_create'
),
url(
r'^setup/smart_link/condition/(?P<smart_link_condition_pk>\d+)/edit/$',
'smart_link_condition_edit', name='smart_link_condition_edit'
),
url(
r'^setup/smart_link/condition/(?P<smart_link_condition_pk>\d+)/delete/$',
'smart_link_condition_delete', name='smart_link_condition_delete'
),
)

View File

@@ -43,19 +43,29 @@ class SetupSmartLinkDocumentTypesView(AssignRemoveView):
return get_object_or_404(SmartLink, pk=self.kwargs['pk'])
def left_list(self):
return AssignRemoveView.generate_choices(DocumentType.objects.exclude(pk__in=self.get_object().document_types.all()))
return AssignRemoveView.generate_choices(
DocumentType.objects.exclude(
pk__in=self.get_object().document_types.all()
)
)
def right_list(self):
return AssignRemoveView.generate_choices(self.get_object().document_types.all())
return AssignRemoveView.generate_choices(
self.get_object().document_types.all()
)
def remove(self, item):
self.get_object().document_types.remove(item)
def get_context_data(self, **kwargs):
data = super(SetupSmartLinkDocumentTypesView, self).get_context_data(**kwargs)
data = super(
SetupSmartLinkDocumentTypesView, self
).get_context_data(**kwargs)
data.update({
'object': self.get_object(),
'title': _('Document type for which to enable smart link: %s') % self.get_object(),
'title': _(
'Document type for which to enable smart link: %s'
) % self.get_object(),
})
return data
@@ -63,20 +73,34 @@ class SetupSmartLinkDocumentTypesView(AssignRemoveView):
class ResolvedSmartLinkView(DocumentListView):
def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(Document, pk=self.kwargs['document_pk'])
self.smart_link = get_object_or_404(SmartLink, pk=self.kwargs['smart_link_pk'])
self.document = get_object_or_404(
Document, pk=self.kwargs['document_pk']
)
self.smart_link = get_object_or_404(
SmartLink, pk=self.kwargs['smart_link_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, self.document)
AccessControlList.objects.check_access(
permission_document_view, request.user, self.document
)
try:
Permission.check_permissions(request.user, [permission_smart_link_view])
Permission.check_permissions(
request.user, [permission_smart_link_view]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_smart_link_view, request.user, self.smart_link)
AccessControlList.objects.check_access(
permission_smart_link_view, request.user, self.smart_link
)
return super(ResolvedSmartLinkView, self).dispatch(request, *args, **kwargs)
return super(
ResolvedSmartLinkView, self
).dispatch(request, *args, **kwargs)
def get_document_queryset(self):
try:
@@ -85,7 +109,9 @@ class ResolvedSmartLinkView(DocumentListView):
queryset = Document.objects.none()
if self.request.user.is_staff or self.request.user.is_superuser:
messages.error(self.request, _('Smart link query error: %s' % exception))
messages.error(
self.request, _('Smart link query error: %s' % exception)
)
return queryset
@@ -93,7 +119,10 @@ class ResolvedSmartLinkView(DocumentListView):
return {
'hide_links': True,
'object': self.document,
'title': _('Documents in smart link "%(smart_link)s" as relation to "%(document)s"') % {
'title': _(
'Documents in smart link "%(smart_link)s" as relation to '
'"%(document)s"'
) % {
'document': self.document,
'smart_link': self.smart_link.get_dynamic_label(self.document),
}
@@ -114,7 +143,11 @@ class SmartLinkListView(SingleObjectListView):
return {
'extra_columns': [
{'name': _('Dynamic label'), 'attribute': 'dynamic_label'},
{'name': _('Enabled'), 'attribute': encapsulate(lambda instance: two_state_template(instance.enabled))},
{
'name': _('Enabled'), 'attribute': encapsulate(
lambda instance: two_state_template(instance.enabled)
)
},
],
'hide_link': True,
'title': _('Smart links'),
@@ -126,20 +159,34 @@ class DocumentSmartLinkListView(SmartLinkListView):
self.document = get_object_or_404(Document, pk=self.kwargs['pk'])
try:
Permission.check_permissions(request.user, (permission_document_view,))
Permission.check_permissions(
request.user, (permission_document_view,)
)
except PermissionDenied:
AccessControlList.objects.check_permissions(permission_document_view, request.user, self.document)
AccessControlList.objects.check_permissions(
permission_document_view, request.user, self.document
)
return super(DocumentSmartLinkListView, self).dispatch(request, *args, **kwargs)
return super(
DocumentSmartLinkListView, self
).dispatch(request, *args, **kwargs)
def get_smart_link_queryset(self):
return ResolvedSmartLink.objects.filter(document_types=self.document.document_type, enabled=True)
return ResolvedSmartLink.objects.filter(
document_types=self.document.document_type, enabled=True
)
def get_extra_context(self):
return {
'document': self.document,
'extra_columns': (
{'name': _('Label'), 'attribute': encapsulate(lambda smart_link: smart_link.get_dynamic_label(self.document))},
{
'name': _('Label'), 'attribute': encapsulate(
lambda smart_link: smart_link.get_dynamic_label(
self.document
)
)
},
),
'hide_object': True,
'hide_link': True,
@@ -149,13 +196,19 @@ class DocumentSmartLinkListView(SmartLinkListView):
def smart_link_create(request):
Permission.check_permissions(request.user, [permission_smart_link_create])
Permission.check_permissions(
request.user, [permission_smart_link_create]
)
if request.method == 'POST':
form = SmartLinkForm(request.POST)
if form.is_valid():
document_group = form.save()
messages.success(request, _('Smart link: %s created successfully.') % document_group)
messages.success(
request, _(
'Smart link: %s created successfully.'
) % document_group
)
return HttpResponseRedirect(reverse('linking:smart_link_list'))
else:
form = SmartLinkForm()
@@ -170,15 +223,23 @@ def smart_link_edit(request, smart_link_pk):
smart_link = get_object_or_404(SmartLink, pk=smart_link_pk)
try:
Permission.check_permissions(request.user, [permission_smart_link_edit])
Permission.check_permissions(
request.user, [permission_smart_link_edit]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_smart_link_edit, request.user, smart_link)
AccessControlList.objects.check_access(
permission_smart_link_edit, request.user, smart_link
)
if request.method == 'POST':
form = SmartLinkForm(request.POST, instance=smart_link)
if form.is_valid():
smart_link = form.save()
messages.success(request, _('Smart link: %s edited successfully.') % smart_link)
messages.success(
request, _(
'Smart link: %s edited successfully.'
) % smart_link
)
return HttpResponseRedirect(reverse('linking:smart_link_list'))
else:
form = SmartLinkForm(instance=smart_link)
@@ -194,22 +255,39 @@ def smart_link_delete(request, smart_link_pk):
smart_link = get_object_or_404(SmartLink, pk=smart_link_pk)
try:
Permission.check_permissions(request.user, [permission_smart_link_delete])
Permission.check_permissions(
request.user, [permission_smart_link_delete]
)
except PermissionDenied:
AccessControlList.objects.check_access(permission_smart_link_delete, request.user, smart_link)
AccessControlList.objects.check_access(
permission_smart_link_delete, request.user, smart_link
)
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:
smart_link.delete()
messages.success(request, _('Smart link: %s deleted successfully.') % smart_link)
messages.success(
request, _(
'Smart link: %s deleted successfully.'
) % smart_link
)
except Exception as exception:
messages.error(request, _('Error deleting smart link: %(smart_link)s; %(exception)s.') % {
messages.error(
request, _(
'Error deleting smart link: %(smart_link)s; '
'%(exception)s.'
) % {
'smart_link': smart_link,
'exception': exception
})
}
)
return HttpResponseRedirect(next)
return render_to_response('appearance/generic_confirm.html', {
@@ -225,15 +303,24 @@ def smart_link_condition_list(request, smart_link_pk):
smart_link = get_object_or_404(SmartLink, pk=smart_link_pk)
try:
Permission.check_permissions(request.user, [permission_smart_link_edit])
Permission.check_permissions(
request.user, [permission_smart_link_edit]
)
except PermissionDenied:
AccessControlList.objects.check_access([permission_smart_link_edit], request.user, smart_link)
AccessControlList.objects.check_access(
[permission_smart_link_edit], request.user, smart_link
)
return render_to_response('appearance/generic_list.html', {
'title': _('Conditions for smart link: %s') % smart_link,
'object_list': smart_link.conditions.all(),
'extra_columns': [
{'name': _('Enabled'), 'attribute': encapsulate(lambda x: two_state_template(x.enabled))},
{
'name': _('Enabled'),
'attribute': encapsulate(
lambda x: two_state_template(x.enabled)
)
},
],
'hide_link': True,
'object': smart_link,
@@ -244,9 +331,13 @@ def smart_link_condition_create(request, smart_link_pk):
smart_link = get_object_or_404(SmartLink, pk=smart_link_pk)
try:
Permission.check_permissions(request.user, [permission_smart_link_edit])
Permission.check_permissions(
request.user, [permission_smart_link_edit]
)
except PermissionDenied:
AccessControlList.objects.check_access([permission_smart_link_edit], request.user, smart_link)
AccessControlList.objects.check_access(
[permission_smart_link_edit], request.user, smart_link
)
if request.method == 'POST':
form = SmartLinkConditionForm(data=request.POST)
@@ -254,8 +345,16 @@ def smart_link_condition_create(request, smart_link_pk):
new_smart_link_condition = form.save(commit=False)
new_smart_link_condition.smart_link = smart_link
new_smart_link_condition.save()
messages.success(request, _('Smart link condition: "%s" created successfully.') % new_smart_link_condition)
return HttpResponseRedirect(reverse('linking:smart_link_condition_list', args=[smart_link.pk]))
messages.success(
request, _(
'Smart link condition: "%s" created successfully.'
) % new_smart_link_condition
)
return HttpResponseRedirect(
reverse(
'linking:smart_link_condition_list', args=[smart_link.pk]
)
)
else:
form = SmartLinkConditionForm()
@@ -267,21 +366,34 @@ def smart_link_condition_create(request, smart_link_pk):
def smart_link_condition_edit(request, smart_link_condition_pk):
smart_link_condition = get_object_or_404(SmartLinkCondition, pk=smart_link_condition_pk)
smart_link_condition = get_object_or_404(
SmartLinkCondition, pk=smart_link_condition_pk
)
try:
Permission.check_permissions(request.user, [permission_smart_link_edit])
Permission.check_permissions(
request.user, [permission_smart_link_edit]
)
except PermissionDenied:
AccessControlList.objects.check_access([permission_smart_link_edit], request.user, smart_link_condition.smart_link)
AccessControlList.objects.check_access(
[permission_smart_link_edit], request.user,
smart_link_condition.smart_link
)
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':
form = SmartLinkConditionForm(request.POST, instance=smart_link_condition)
form = SmartLinkConditionForm(
request.POST, instance=smart_link_condition
)
if form.is_valid():
smart_link_condition = form.save()
messages.success(request, _('Smart link condition: "%s" edited successfully.') % smart_link_condition)
messages.success(
request, _(
'Smart link condition: "%s" edited successfully.'
) % smart_link_condition
)
return HttpResponseRedirect(next)
else:
form = SmartLinkConditionForm(instance=smart_link_condition)
@@ -298,12 +410,17 @@ def smart_link_condition_edit(request, smart_link_condition_pk):
def smart_link_condition_delete(request, smart_link_condition_pk):
smart_link_condition = get_object_or_404(SmartLinkCondition, pk=smart_link_condition_pk)
smart_link_condition = get_object_or_404(
SmartLinkCondition, pk=smart_link_condition_pk
)
try:
Permission.check_permissions(request.user, [permission_smart_link_edit])
except PermissionDenied:
AccessControlList.objects.check_access([permission_smart_link_edit], request.user, smart_link_condition.smart_link)
AccessControlList.objects.check_access(
[permission_smart_link_edit], request.user,
smart_link_condition.smart_link
)
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))))
@@ -311,12 +428,21 @@ def smart_link_condition_delete(request, smart_link_condition_pk):
if request.method == 'POST':
try:
smart_link_condition.delete()
messages.success(request, _('Smart link condition: "%s" deleted successfully.') % smart_link_condition)
messages.success(
request, _(
'Smart link condition: "%s" deleted successfully.'
) % smart_link_condition
)
except Exception as exception:
messages.error(request, _('Error deleting smart link condition: %(smart_link_condition)s; %(exception)s.') % {
messages.error(
request, _(
'Error deleting smart link condition: '
'%(smart_link_condition)s; %(exception)s.'
) % {
'smart_link_condition': smart_link_condition,
'exception': exception
})
}
)
return HttpResponseRedirect(next)
return render_to_response('appearance/generic_confirm.html', {

View File

@@ -42,7 +42,10 @@ class LockManager(models.Manager):
logger.debug('unable to acquire lock: %s', name)
raise LockError('Unable to acquire lock')
except OperationalError as exception:
raise LockError('Operational error while trying to acquire lock: %s; %s', name, exception)
raise LockError(
'Operational error while trying to acquire lock: %s; %s',
name, exception
)
else:
logger.debug('acquired lock: %s', name)
return lock

View File

@@ -10,9 +10,15 @@ from .settings import DEFAULT_LOCK_TIMEOUT
@python_2_unicode_compatible
class Lock(models.Model):
creation_datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Creation datetime'))
timeout = models.IntegerField(default=DEFAULT_LOCK_TIMEOUT, verbose_name=_('Timeout'))
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'))
creation_datetime = models.DateTimeField(
auto_now_add=True, verbose_name=_('Creation datetime')
)
timeout = models.IntegerField(
default=DEFAULT_LOCK_TIMEOUT, verbose_name=_('Timeout')
)
name = models.CharField(
max_length=64, unique=True, verbose_name=_('Name')
)
objects = LockManager()
@@ -27,7 +33,9 @@ class Lock(models.Model):
def release(self):
try:
lock = Lock.objects.get(name=self.name, creation_datetime=self.creation_datetime)
lock = Lock.objects.get(
name=self.name, creation_datetime=self.creation_datetime
)
except Lock.DoesNotExist:
# Our lock has expired and was reassigned
pass

View File

@@ -4,4 +4,6 @@ from django.conf import settings
DEFAULT_LOCK_TIMEOUT_VALUE = 30
DEFAULT_LOCK_TIMEOUT = getattr(settings, 'LOCK_MANAGER_DEFAULT_LOCK_TIMEOUT', DEFAULT_LOCK_TIMEOUT_VALUE)
DEFAULT_LOCK_TIMEOUT = getattr(
settings, 'LOCK_MANAGER_DEFAULT_LOCK_TIMEOUT', DEFAULT_LOCK_TIMEOUT_VALUE
)

View File

@@ -14,9 +14,24 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='DocumentMetadata',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('value', models.CharField(db_index=True, max_length=255, null=True, verbose_name='Value', blank=True)),
('document', models.ForeignKey(related_name='metadata', verbose_name='Document', to='documents.Document')),
(
'id', models.AutoField(
verbose_name='ID', serialize=False,
auto_created=True, primary_key=True
)
),
(
'value', models.CharField(
db_index=True, max_length=255, null=True,
verbose_name='Value', blank=True
)
),
(
'document', models.ForeignKey(
related_name='metadata', verbose_name='Document',
to='documents.Document'
)
),
],
options={
'verbose_name': 'Document metadata',
@@ -27,9 +42,23 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='DocumentTypeMetadataType',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('required', models.BooleanField(default=False, verbose_name='Required')),
('document_type', models.ForeignKey(related_name='metadata', verbose_name='Document type', to='documents.DocumentType')),
(
'id', models.AutoField(
verbose_name='ID', serialize=False,
auto_created=True, primary_key=True
)
),
(
'required', models.BooleanField(
default=False, verbose_name='Required'
)
),
(
'document_type', models.ForeignKey(
related_name='metadata',
verbose_name='Document type', to='documents.DocumentType'
)
),
],
options={
'verbose_name': 'Document type metadata type options',
@@ -40,12 +69,47 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='MetadataType',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(help_text='Do not use python reserved words, or spaces.', unique=True, max_length=48, verbose_name='Name')),
('title', models.CharField(max_length=48, verbose_name='Title')),
('default', models.CharField(help_text='Enter a string to be evaluated.', max_length=128, null=True, verbose_name='Default', blank=True)),
('lookup', models.TextField(help_text='Enter a string to be evaluated that returns an iterable.', null=True, verbose_name='Lookup', blank=True)),
('validation', models.CharField(blank=True, max_length=64, verbose_name='Validation function name', choices=[('Parse date', 'Parse date'), ('Parse date and time', 'Parse date and time'), ('Parse time', 'Parse time')])),
(
'id', models.AutoField(
verbose_name='ID', serialize=False,
auto_created=True, primary_key=True
)
),
(
'name', models.CharField(
help_text='Do not use python reserved words, '
'or spaces.', unique=True, max_length=48,
verbose_name='Name'
)
),
(
'title', models.CharField(
max_length=48, verbose_name='Title'
)
),
(
'default', models.CharField(
help_text='Enter a string to be evaluated.',
max_length=128, null=True, verbose_name='Default', blank=True
)
),
(
'lookup', models.TextField(
help_text='Enter a string to be evaluated that '
'returns an iterable.', null=True,
verbose_name='Lookup', blank=True)
),
(
'validation', models.CharField(
blank=True, max_length=64,
verbose_name='Validation function name',
choices=[
('Parse date', 'Parse date'),
('Parse date and time', 'Parse date and time'),
('Parse time', 'Parse time')
]
)
),
],
options={
'ordering': ('title',),
@@ -57,7 +121,9 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='documenttypemetadatatype',
name='metadata_type',
field=models.ForeignKey(verbose_name='Metadata type', to='metadata.MetadataType'),
field=models.ForeignKey(
verbose_name='Metadata type', to='metadata.MetadataType'
),
preserve_default=True,
),
migrations.AlterUniqueTogether(
@@ -67,7 +133,9 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='documentmetadata',
name='metadata_type',
field=models.ForeignKey(verbose_name='Type', to='metadata.MetadataType'),
field=models.ForeignKey(
verbose_name='Type', to='metadata.MetadataType'
),
preserve_default=True,
),
migrations.AlterUniqueTogether(

View File

@@ -13,7 +13,10 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='metadatatype',
options={'ordering': ('label',), 'verbose_name': 'Metadata type', 'verbose_name_plural': 'Metadata types'},
options={
'ordering': ('label',), 'verbose_name': 'Metadata type',
'verbose_name_plural': 'Metadata types'
},
),
migrations.RenameField(
model_name='metadatatype',

View File

@@ -11,8 +11,8 @@ 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
)
view, 'mayan_object_permissions', {}
).get(request.method, None)
if required_permission:
try:

View File

@@ -13,8 +13,8 @@ from permissions import Permission
class MayanPermission(BasePermission):
def has_permission(self, request, view):
required_permission = getattr(
view, 'mayan_view_permissions', {}).get(request.method, None
)
view, 'mayan_view_permissions', {}
).get(request.method, None)
if required_permission:
try:
@@ -28,8 +28,8 @@ class MayanPermission(BasePermission):
def has_object_permission(self, request, view, obj):
required_permission = getattr(
view, 'mayan_object_permissions', {}).get(request.method, None
)
view, 'mayan_object_permissions', {}
).get(request.method, None)
if required_permission:
try:

View File

@@ -8,7 +8,8 @@ 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'
r'^(?P<app_name>\w+)/$', APIAppView.as_view(),
name='api-version-0-app'
),
)

View File

@@ -46,7 +46,9 @@ class Version_0(generics.GenericAPIView):
{
'name': unicode(endpoint),
'url': reverse('api-version-0-app',
args=[unicode(endpoint)], request=request, format=format)
args=[unicode(endpoint)], request=request,
format=format
)
} for endpoint in APIEndPoint.get_all()
],
})

View File

@@ -40,7 +40,9 @@ class SmartSettingsApp(MayanAppConfig):
)
SourceColumn(
source=Setting, label=_('Found in path'),
attribute=encapsulate(lambda instance: exists_widget(instance.value) if instance.is_path else _('n/a'))
attribute=encapsulate(
lambda instance: exists_widget(instance.value) if instance.is_path else _('n/a')
)
)
menu_object.bind_links(
@@ -54,4 +56,6 @@ class SmartSettingsApp(MayanAppConfig):
except ImportError:
logger.debug('App %s has not settings.py file', app.name)
else:
logger.debug('Imported settings.py file for app %s', app.name)
logger.debug(
'Imported settings.py file for app %s', app.name
)

View File

@@ -7,10 +7,11 @@ from .views import NamespaceDetailView, NamespaceListView
urlpatterns = patterns(
'',
url(
r'^namespace/all/$', NamespaceListView.as_view(), name='namespace_list'
r'^namespace/all/$', NamespaceListView.as_view(),
name='namespace_list'
),
url(
r'^namespace/(?P<namespace_name>\w+)/$', NamespaceDetailView.as_view(),
name='namespace_detail'
r'^namespace/(?P<namespace_name>\w+)/$',
NamespaceDetailView.as_view(), name='namespace_detail'
),
)

View File

@@ -49,7 +49,11 @@ class SourcesApp(MayanAppConfig):
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.'),
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'
)
@@ -131,7 +135,8 @@ class SourcesApp(MayanAppConfig):
)
post_upgrade.connect(
initialize_periodic_tasks, dispatch_uid='initialize_periodic_tasks'
initialize_periodic_tasks,
dispatch_uid='initialize_periodic_tasks'
)
post_initial_setup.connect(
create_default_document_source,