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], links=[link_acl_permissions, link_acl_delete],
sources=[AccessControlList] 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_factory(variable_name):
def get_kwargs(context): def get_kwargs(context):
content_type = ContentType.objects.get_for_model(context[variable_name]) 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 return get_kwargs

View File

@@ -92,7 +92,13 @@ class AccessControlListManager(models.Manager):
content_type=parent_content_type, role__in=user_roles, content_type=parent_content_type, role__in=user_roles,
permissions=permission.stored_permission 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: else:
parent_acl_query = Q() parent_acl_query = Q()

View File

@@ -15,12 +15,34 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='AccessEntry', name='AccessEntry',
fields=[ 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()), ('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')), 'content_type', models.ForeignKey(
('permission', models.ForeignKey(verbose_name='Permission', to='permissions.StoredPermission')), 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={ options={
'verbose_name': 'Access entry', 'verbose_name': 'Access entry',
@@ -31,7 +53,12 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CreatorSingleton', name='CreatorSingleton',
fields=[ 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={ options={
'verbose_name': 'Creator', 'verbose_name': 'Creator',
@@ -42,11 +69,31 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='DefaultAccessEntry', name='DefaultAccessEntry',
fields=[ 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()), ('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')), 'content_type', models.ForeignKey(
('permission', models.ForeignKey(verbose_name='Permission', to='permissions.StoredPermission')), 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={ options={
'verbose_name': 'Default access entry', 'verbose_name': 'Default access entry',

View File

@@ -16,11 +16,31 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='AccessControlList', name='AccessControlList',
fields=[ 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()), ('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)), 'content_type', models.ForeignKey(
('role', models.ForeignKey(related_name='acls', verbose_name='Role', to='permissions.Role')), 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={ options={
'verbose_name': 'Access entry', 'verbose_name': 'Access entry',

View File

@@ -31,7 +31,10 @@ class AccessControlList(models.Model):
fk_field='object_id', fk_field='object_id',
) )
# TODO: limit choices to the permissions valid for the content_object # 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')) role = models.ForeignKey(Role, related_name='acls', verbose_name=_('Role'))
objects = AccessControlListManager() objects = AccessControlListManager()
@@ -45,4 +48,6 @@ class AccessControlList(models.Model):
return '{} <=> {}'.format(self.content_object, self.role) return '{} <=> {}'.format(self.content_object, self.role)
def get_inherited_permissions(self): 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')) namespace = PermissionNamespace('acls', _('Access control lists'))
permission_acl_edit = namespace.add_permission(name='acl_edit', label=_('Edit ACLs')) permission_acl_edit = namespace.add_permission(
permission_acl_view = namespace.add_permission(name='acl_view', label=_('View ACLs')) 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): class PermissionTestCase(TestCase):
def setUp(self): 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 = self.document_type_1.ocr_settings
ocr_settings.auto_ocr = False ocr_settings.auto_ocr = False
ocr_settings.save() 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 = self.document_type_2.ocr_settings
ocr_settings.auto_ocr = False ocr_settings.auto_ocr = False
ocr_settings.save() ocr_settings.save()
with open(TEST_SMALL_DOCUMENT_PATH) as file_object: 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: 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: 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.user = get_user_model().objects.create(username='test user')
self.group = Group.objects.create(name='test group') self.group = Group.objects.create(name='test group')
@@ -52,23 +62,35 @@ class PermissionTestCase(TestCase):
def test_check_access_without_permissions(self): def test_check_access_without_permissions(self):
with self.assertRaises(PermissionDenied): 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): def test_filtering_without_permissions(self):
self.assertEqual( 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): def test_check_access_with_acl(self):
self.group.user_set.add(self.user) self.group.user_set.add(self.user)
self.role.groups.add(self.group) 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) acl.permissions.add(permission_document_view.stored_permission)
try: 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: except PermissionDenied:
self.fail('PermissionDenied exception was not expected.') 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.permissions.add(permission_document_view.stored_permission)
self.role.groups.add(self.group) 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) acl.permissions.add(permission_document_view.stored_permission)
self.assertEqual( self.assertEqual(
list(AccessControlList.objects.filter_by_access(permission=permission_document_view, user=self.user, queryset=Document.objects.all())), list(
[self.document_1] 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): def test_check_access_with_inherited_acl(self):
self.group.user_set.add(self.user) self.group.user_set.add(self.user)
self.role.groups.add(self.group) 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.permissions.add(permission_document_view.stored_permission)
try: 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: except PermissionDenied:
self.fail('PermissionDenied exception was not expected.') self.fail('PermissionDenied exception was not expected.')
@@ -101,14 +134,21 @@ class PermissionTestCase(TestCase):
self.group.user_set.add(self.user) self.group.user_set.add(self.user)
self.role.groups.add(self.group) 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.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) acl.permissions.add(permission_document_view.stored_permission)
try: 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: except PermissionDenied:
self.fail('PermissionDenied exception was not expected.') 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.permissions.add(permission_document_view.stored_permission)
self.role.groups.add(self.group) 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.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_1 in result)
self.assertTrue(self.document_2 in result) self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 not 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.permissions.add(permission_document_view.stored_permission)
self.role.groups.add(self.group) 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.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) 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_1 in result)
self.assertTrue(self.document_2 in result) self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 in result) self.assertTrue(self.document_3 in result)

View File

@@ -6,8 +6,17 @@ from .views import ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView
urlpatterns = patterns( urlpatterns = patterns(
'acls.views', 'acls.views',
url(r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/new/$', ACLCreateView.as_view(), name='acl_new'), url(
url(r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/list/$', ACLListView.as_view(), name='acl_list'), 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+)/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): class ACLListView(SingleObjectListView):
@staticmethod @staticmethod
def permission_titles(permission_list): 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): 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: 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: except self.content_type.model_class().DoesNotExist:
raise Http404 raise Http404
try: try:
Permission.check_permissions(request.user, permissions=(permission_acl_view,)) Permission.check_permissions(
request.user, permissions=(permission_acl_view,)
)
except PermissionDenied: 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) return super(ACLListView, self).dispatch(request, *args, **kwargs)
def get_queryset(self): 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): def get_extra_context(self):
return { return {
@@ -60,7 +73,11 @@ class ACLListView(SingleObjectListView):
}, },
{ {
'name': _('Permissions'), '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 model = AccessControlList
def dispatch(self, request, *args, **kwargs): 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: 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: except content_type.model_class().DoesNotExist:
raise Http404 raise Http404
try: try:
Permission.check_permissions(request.user, permissions=(permission_acl_edit,)) Permission.check_permissions(
request.user, permissions=(permission_acl_edit,)
)
except PermissionDenied: 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) return super(ACLCreateView, self).dispatch(request, *args, **kwargs)
@@ -98,7 +124,9 @@ class ACLCreateView(SingleObjectCreateView):
def get_extra_context(self): def get_extra_context(self):
return { return {
'object': self.content_object, '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']) acl = get_object_or_404(AccessControlList, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(request.user, permissions=(permission_acl_edit,)) Permission.check_permissions(
request.user, permissions=(permission_acl_edit,)
)
except PermissionDenied: 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) return super(ACLDeleteView, self).dispatch(request, *args, **kwargs)
@@ -136,8 +168,12 @@ class ACLPermissionsView(AssignRemoveView):
results = [] results = []
for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace): for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace):
permission_options = [(unicode(permission.pk), permission) for permission in permissions] permission_options = [
results.append((PermissionNamespace.get(namespace), permission_options)) (unicode(permission.pk), permission) for permission in permissions
]
results.append(
(PermissionNamespace.get(namespace), permission_options)
)
return results return results
@@ -149,15 +185,23 @@ class ACLPermissionsView(AssignRemoveView):
acl = get_object_or_404(AccessControlList, pk=self.kwargs['pk']) acl = get_object_or_404(AccessControlList, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(request.user, permissions=(permission_acl_edit,)) Permission.check_permissions(
request.user, permissions=(permission_acl_edit,)
)
except PermissionDenied: 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): def get_right_list_help_text(self):
if self.get_object().get_inherited_permissions(): 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 return None
@@ -165,14 +209,24 @@ class ACLPermissionsView(AssignRemoveView):
return get_object_or_404(AccessControlList, pk=self.kwargs['pk']) return get_object_or_404(AccessControlList, pk=self.kwargs['pk'])
def get_available_list(self): 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): def get_disabled_choices(self):
""" """
Get permissions from a parent's acls but remove the permissions we Get permissions from a parent's acls but remove the permissions we
already hold for this object 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): def get_extra_context(self):
return { return {

View File

@@ -8,6 +8,7 @@ namespace = Namespace(name='authentication', label=_('Authentication'))
setting_login_method = namespace.add_setting( setting_login_method = namespace.add_setting(
global_name='AUTHENTICATION_LOGIN_METHOD', default='username', global_name='AUTHENTICATION_LOGIN_METHOD', default='username',
help_text=_( 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

@@ -140,12 +140,12 @@ def checkout_info(request, document_pk):
return render_to_response( return render_to_response(
'appearance/generic_template.html', { 'appearance/generic_template.html', {
'paragraphs': paragraphs, 'paragraphs': paragraphs,
'object': document, 'object': document,
'title': _('Check out details for document: %s') % document 'title': _('Check out details for document: %s') % document
}, },
context_instance=RequestContext(request) context_instance=RequestContext(request)
) )
def checkin_document(request, document_pk): def checkin_document(request, document_pk):

View File

@@ -300,8 +300,10 @@ class TransformationZoom(BaseTransformation):
decimal_value = float(self.percent) / 100 decimal_value = float(self.percent) / 100
return self.image.resize( 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,10 +69,12 @@ class TransformationDeleteView(SingleObjectDeleteView):
] ]
), ),
'title': _( 'title': _(
'Delete transformation "%(transformation)s" for: %(content_object)s?') % { 'Delete transformation "%(transformation)s" for: '
'transformation': self.transformation, '%(content_object)s?'
'content_object': self.transformation.content_object ) % {
}, 'transformation': self.transformation,
'content_object': self.transformation.content_object
},
'transformation': self.transformation, 'transformation': self.transformation,
} }

View File

@@ -29,12 +29,19 @@ def comment_delete(request, comment_id=None, comment_id_list=None):
if comment_id: if comment_id:
comments = [get_object_or_404(Comment, pk=comment_id)] comments = [get_object_or_404(Comment, pk=comment_id)]
elif comment_id_list: 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: try:
Permission.check_permissions(request.user, [permission_comment_delete]) Permission.check_permissions(request.user, [permission_comment_delete])
except PermissionDenied: 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: if not comments:
messages.error(request, _('Must provide at least one comment.')) 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: for comment in comments:
try: try:
comment.delete() comment.delete()
messages.success(request, _('Comment "%s" deleted successfully.') % comment) messages.success(
request, _('Comment "%s" deleted successfully.') % comment
)
except Exception as exception: except Exception as exception:
messages.error(request, _('Error deleting comment "%(comment)s": %(error)s') % { messages.error(
'comment': comment, 'error': exception request, _(
}) 'Error deleting comment "%(comment)s": %(error)s'
) % {
'comment': comment, 'error': exception
}
)
return HttpResponseRedirect(next) return HttpResponseRedirect(next)

View File

@@ -45,9 +45,13 @@ def document_version_post_save_hook(instance):
logger.debug('instance: %s', instance) logger.debug('instance: %s', instance)
try: try:
document_signature = DocumentVersionSignature.objects.get(document_version=instance) document_signature = DocumentVersionSignature.objects.get(
document_version=instance
)
except DocumentVersionSignature.DoesNotExist: 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() document_signature.check_for_embedded_signature()
@@ -60,7 +64,9 @@ class DocumentSignaturesApp(MayanAppConfig):
def ready(self): def ready(self):
super(DocumentSignaturesApp, self).ready() 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) DocumentVersion.register_pre_open_hook(1, document_pre_open_hook)
ModelPermission.register( ModelPermission.register(
@@ -70,5 +76,18 @@ class DocumentSignaturesApp(MayanAppConfig):
) )
) )
menu_facet.bind_links(links=[link_document_verify], sources=[Document]) menu_facet.bind_links(
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']) 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): 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): 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_delete = Link(
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]) condition=can_delete_detached_signature,
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') permissions=[permission_signature_delete], tags='dangerous',
link_document_verify = Link(permissions=[permission_document_verify], text=_('Signatures'), view='signatures:document_verify', args='object.pk') 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: 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: else:
if document_signature.signature_file: if document_signature.signature_file:
logger.debug('Existing detached signature') logger.debug('Existing detached signature')
@@ -37,7 +39,9 @@ class DocumentVersionSignatureManager(models.Manager):
def has_detached_signature(self, document_version): def has_detached_signature(self, document_version):
try: try:
document_signature = self.get_document_signature(document_version=document_version) document_signature = self.get_document_signature(
document_version=document_version
)
except ValueError: except ValueError:
return False return False
else: else:
@@ -50,23 +54,31 @@ class DocumentVersionSignatureManager(models.Manager):
logger.debug('document_version: %s', document_version) logger.debug('document_version: %s', document_version)
try: try:
document_signature = self.get_document_signature(document_version=document_version) document_signature = self.get_document_signature(
document_version=document_version
)
except ValueError: except ValueError:
return False return False
else: else:
return document_signature.has_embedded_signature return document_signature.has_embedded_signature
def detached_signature(self, document_version): 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): def verify_signature(self, document_version):
document_version_descriptor = document_version.open(raw=True) document_version_descriptor = document_version.open(raw=True)
detached_signature = None detached_signature = None
if self.has_detached_signature(document_version=document_version): if self.has_detached_signature(document_version=document_version):
logger.debug('has detached signature') 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) args = (document_version_descriptor, detached_signature)
else: else:
args = (document_version_descriptor,) args = (document_version_descriptor,)
@@ -81,7 +93,9 @@ class DocumentVersionSignatureManager(models.Manager):
detached_signature.close() detached_signature.close()
def clear_detached_signature(self, document_version): 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: if not document_signature.signature_file:
raise Exception('document doesn\'t have a detached signature') 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 Model that describes a document version signature properties
""" """
document_version = models.ForeignKey(DocumentVersion, editable=False, verbose_name=_('Document version')) document_version = models.ForeignKey(
signature_file = models.FileField(blank=True, null=True, storage=storage_backend, upload_to=upload_to, verbose_name=_('Signature file')) DocumentVersion, editable=False, verbose_name=_('Document version')
has_embedded_signature = models.BooleanField(default=False, verbose_name=_('Has embedded signature')) )
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() objects = DocumentVersionSignatureManager()
@@ -33,7 +40,9 @@ class DocumentVersionSignature(models.Model):
logger.debug('checking for embedded signature') logger.debug('checking for embedded signature')
with self.document_version.open(raw=True) as file_object: 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() self.save()
def delete_detached_signature_file(self): def delete_detached_signature_file(self):

View File

@@ -4,9 +4,19 @@ from django.utils.translation import ugettext_lazy as _
from permissions import PermissionNamespace 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_document_verify = namespace.add_permission(
permission_signature_delete = namespace.add_permission(name='signature_delete', label=_('Delete detached signatures')) name='document_verify', label=_('Verify document 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_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 from smart_settings import Namespace
namespace = Namespace(name='signatures', label=_('Document signatures')) 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 from .models import DocumentVersionSignature
TEST_SIGNED_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg') TEST_SIGNED_DOCUMENT_PATH = os.path.join(
TEST_SIGNATURE_FILE_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.sig') settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg'
TEST_KEY_FILE = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'key0x5F3F7F75D210724D.asc') )
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): class DocumentTestCase(TestCase):
def setUp(self): 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 = self.document_type.ocr_settings
ocr_settings.auto_ocr = False ocr_settings.auto_ocr = False
ocr_settings.save() ocr_settings.save()
with open(TEST_DOCUMENT_PATH) as file_object: 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: with open(TEST_KEY_FILE) as file_object:
gpg.import_key(file_object.read()) gpg.import_key(file_object.read())
@@ -37,24 +48,52 @@ class DocumentTestCase(TestCase):
self.document_type.delete() self.document_type.delete()
def test_document_no_signature(self): 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): def test_new_document_version_signed(self):
with open(TEST_SIGNED_DOCUMENT_PATH) as file_object: 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(
self.assertEqual(DocumentVersionSignature.objects.verify_signature(self.document.latest_version).status, SIGNATURE_STATE_VALID) 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): def test_detached_signatures(self):
with open(TEST_DOCUMENT_PATH) as file_object: 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 # 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: 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(
self.assertEqual(DocumentVersionSignature.objects.verify_signature(self.document.latest_version).status, SIGNATURE_STATE_VALID) 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): def ready(self):
super(DocumentStatesApp, self).ready() 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(
SourceColumn(source=WorkflowInstance, label=_('User'), attribute=encapsulate(lambda workflow: getattr(workflow.get_last_log_entry(), 'user', _('None')))) source=WorkflowInstance, label=_('Current state'),
SourceColumn(source=WorkflowInstance, label=_('Last transition'), attribute='get_last_transition') attribute='get_current_state'
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=_('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(
SourceColumn(source=WorkflowInstanceLogEntry, label=_('User'), attribute='user') source=WorkflowInstanceLogEntry, label=_('Date and time'),
SourceColumn(source=WorkflowInstanceLogEntry, label=_('Transition'), attribute='transition') attribute='datetime'
SourceColumn(source=WorkflowInstanceLogEntry, label=_('Comment'), attribute='comment') )
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(
SourceColumn(source=WorkflowState, label=_('Completion'), attribute='completion') 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(
SourceColumn(source=WorkflowTransition, label=_('Destination state'), attribute='destination_state') 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_facet.bind_links(
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]) links=[link_document_workflow_instance_list], sources=[Document]
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(
menu_object.bind_links(links=[link_workflow_instance_detail, link_workflow_instance_transition], sources=[WorkflowInstance]) links=[
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']) 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_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 from navigation import Link
link_document_workflow_instance_list = Link(text=_('Workflows'), view='document_states:document_workflow_instance_list', args='object.pk') link_document_workflow_instance_list = Link(
link_setup_workflow_create = Link(text=_('Create workflow'), view='document_states:setup_workflow_create') text=_('Workflows'),
link_setup_workflow_delete = Link(tags='dangerous', text=_('Delete'), view='document_states:setup_workflow_delete', args='object.pk') view='document_states:document_workflow_instance_list', 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_create = Link(
link_setup_workflow_list = Link(icon='fa fa-sitemap', text=_('Workflows'), view='document_states:setup_workflow_list') text=_('Create workflow'), view='document_states:setup_workflow_create'
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_delete = Link(
link_setup_workflow_state_edit = Link(text=_('Edit'), view='document_states:setup_workflow_state_edit', args='object.pk') tags='dangerous', text=_('Delete'),
link_setup_workflow_states = Link(text=_('States'), view='document_states:setup_workflow_states', args='object.pk') view='document_states:setup_workflow_delete', 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_document_types = Link(
link_setup_workflow_transition_edit = Link(text=_('Edit'), view='document_states:setup_workflow_transition_edit', args='object.pk') text=_('Document types'),
link_setup_workflow_transitions = Link(text=_('Transitions'), view='document_states:setup_workflow_transitions', args='object.pk') view='document_states:setup_workflow_document_types', 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_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 @python_2_unicode_compatible
class Workflow(models.Model): class Workflow(models.Model):
label = models.CharField(max_length=255, unique=True, verbose_name=_('Label')) label = models.CharField(
document_types = models.ManyToManyField(DocumentType, related_name='workflows', verbose_name=_('Document types')) max_length=255, unique=True, verbose_name=_('Label')
)
document_types = models.ManyToManyField(
DocumentType, related_name='workflows',
verbose_name=_('Document types')
)
objects = WorkflowManager() objects = WorkflowManager()
@@ -36,12 +41,18 @@ class Workflow(models.Model):
def launch_for(self, document): def launch_for(self, document):
try: 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) self.instances.create(document=document)
except IntegrityError: 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: else:
logger.info('Workflow %s launched for document %s', self, document) logger.info(
'Workflow %s launched for document %s', self, document
)
class Meta: class Meta:
verbose_name = _('Workflow') verbose_name = _('Workflow')
@@ -50,10 +61,23 @@ class Workflow(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class WorkflowState(models.Model): 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')) 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')) initial = models.BooleanField(
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')) 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): def __str__(self):
return self.label return self.label
@@ -71,36 +95,54 @@ class WorkflowState(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class WorkflowTransition(models.Model): 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')) label = models.CharField(max_length=255, verbose_name=_('Label'))
origin_state = models.ForeignKey(WorkflowState, related_name='origin_transitions', verbose_name=_('Origin state')) origin_state = models.ForeignKey(
destination_state = models.ForeignKey(WorkflowState, related_name='destination_transitions', verbose_name=_('Destination state')) 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): def __str__(self):
return self.label return self.label
class Meta: class Meta:
unique_together = ('workflow', 'label', 'origin_state', 'destination_state') unique_together = (
'workflow', 'label', 'origin_state', 'destination_state'
)
verbose_name = _('Workflow transition') verbose_name = _('Workflow transition')
verbose_name_plural = _('Workflow transitions') verbose_name_plural = _('Workflow transitions')
@python_2_unicode_compatible @python_2_unicode_compatible
class WorkflowInstance(models.Model): class WorkflowInstance(models.Model):
workflow = models.ForeignKey(Workflow, related_name='instances', verbose_name=_('Workflow')) workflow = models.ForeignKey(
document = models.ForeignKey(Document, related_name='workflows', verbose_name=_('Document')) Workflow, related_name='instances', verbose_name=_('Workflow')
)
document = models.ForeignKey(
Document, related_name='workflows', verbose_name=_('Document')
)
def __str__(self): def __str__(self):
return unicode(self.workflow) return unicode(self.workflow)
def get_absolute_url(self): 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): def do_transition(self, comment, transition, user):
try: try:
if transition in self.get_current_state().origin_transitions.all(): 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: except AttributeError:
# No initial state has been set for this workflow # No initial state has been set for this workflow
pass pass
@@ -134,9 +176,16 @@ class WorkflowInstance(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class WorkflowInstanceLogEntry(models.Model): class WorkflowInstanceLogEntry(models.Model):
workflow_instance = models.ForeignKey(WorkflowInstance, related_name='log_entries', verbose_name=_('Workflow instance')) workflow_instance = models.ForeignKey(
datetime = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_('Datetime')) WorkflowInstance, related_name='log_entries',
transition = models.ForeignKey(WorkflowTransition, verbose_name=_('Transition')) 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')) user = models.ForeignKey(User, verbose_name=_('User'))
comment = models.TextField(blank=True, verbose_name=_('Comment')) comment = models.TextField(blank=True, verbose_name=_('Comment'))

View File

@@ -6,9 +6,22 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('document_states', _('Document workflows')) namespace = PermissionNamespace('document_states', _('Document workflows'))
permission_workflow_create = namespace.add_permission(name='workflow_create', label=_('Create workflows')) permission_workflow_create = namespace.add_permission(
permission_workflow_delete = namespace.add_permission(name='workflow_delte', label=_('Delete workflows')) name='workflow_create', label=_('Create 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_workflow_delete = namespace.add_permission(
permission_document_workflow_view = namespace.add_permission(name='document_workflow_view', label=_('View document workflows')) name='workflow_delte', label=_('Delete workflows')
permission_document_workflow_transition = namespace.add_permission(name='document_workflow_transition', label=_('Transition document 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( urlpatterns = patterns(
'', '',
url(r'^document/(?P<pk>\d+)/workflows/$', DocumentWorkflowInstanceListView.as_view(), name='document_workflow_instance_list'), url(
url(r'^document/workflows/(?P<pk>\d+)/$', WorkflowInstanceDetailView.as_view(), name='workflow_instance_detail'), r'^document/(?P<pk>\d+)/workflows/$',
url(r'^document/workflows/(?P<pk>\d+)/transition/$', WorkflowInstanceTransitionView.as_view(), name='workflow_instance_transition'), 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(
url(r'^setup/create/$', SetupWorkflowCreateView.as_view(), name='setup_workflow_create'), r'^setup/all/$', SetupWorkflowListView.as_view(),
url(r'^setup/(?P<pk>\d+)/edit/$', SetupWorkflowEditView.as_view(), name='setup_workflow_edit'), name='setup_workflow_list'
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(
url(r'^setup/(?P<pk>\d+)/document_types/$', SetupWorkflowDocumentTypesView.as_view(), name='setup_workflow_document_types'), r'^setup/create/$', SetupWorkflowCreateView.as_view(),
url(r'^setup/(?P<pk>\d+)/states/$', SetupWorkflowStateListView.as_view(), name='setup_workflow_states'), name='setup_workflow_create'
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(
url(r'^setup/(?P<pk>\d+)/transitions/create/$', SetupWorkflowTransitionCreateView.as_view(), name='setup_workflow_transition_create'), r'^setup/(?P<pk>\d+)/edit/$', SetupWorkflowEditView.as_view(),
url(r'^setup/workflow/state/(?P<pk>\d+)/delete/$', SetupWorkflowStateDeleteView.as_view(), name='setup_workflow_state_delete'), name='setup_workflow_edit'
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(
url(r'^setup/workflow/transitions/(?P<pk>\d+)/edit/$', SetupWorkflowTransitionEditView.as_view(), name='setup_workflow_transition_edit'), 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): class DocumentWorkflowInstanceListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
Permission.check_permissions(request.user, [permission_document_workflow_view]) Permission.check_permissions(
request.user, [permission_document_workflow_view]
)
except PermissionDenied: 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): def get_document(self):
return get_object_or_404(Document, pk=self.kwargs['pk']) return get_object_or_404(Document, pk=self.kwargs['pk'])
@@ -46,12 +53,16 @@ class DocumentWorkflowInstanceListView(SingleObjectListView):
return self.get_document().workflows.all() return self.get_document().workflows.all()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(DocumentWorkflowInstanceListView, self).get_context_data(**kwargs) context = super(
DocumentWorkflowInstanceListView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
'hide_link': True, 'hide_link': True,
'object': self.get_document(), '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']) self.workflow = get_object_or_404(Workflow, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(request.user, [permission_workflow_view]) Permission.check_permissions(
request.user, [permission_workflow_view]
)
except PermissionDenied: 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): 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): def get_extra_context(self):
return { return {
@@ -83,11 +102,18 @@ class WorkflowDocumentListView(DocumentListView):
class WorkflowInstanceDetailView(SingleObjectListView): class WorkflowInstanceDetailView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
Permission.check_permissions(request.user, [permission_document_workflow_view]) Permission.check_permissions(
request.user, [permission_document_workflow_view]
)
except PermissionDenied: 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): def get_workflow_instance(self):
return get_object_or_404(WorkflowInstance, pk=self.kwargs['pk']) return get_object_or_404(WorkflowInstance, pk=self.kwargs['pk'])
@@ -116,15 +142,27 @@ class WorkflowInstanceTransitionView(FormView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
Permission.check_permissions(request.user, [permission_document_workflow_transition]) Permission.check_permissions(
request.user, [permission_document_workflow_transition]
)
except PermissionDenied: 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): def form_valid(self, form):
transition = self.get_workflow_instance().workflow.transitions.get(pk=form.cleaned_data['transition']) transition = self.get_workflow_instance().workflow.transitions.get(
self.get_workflow_instance().do_transition(comment=form.cleaned_data['comment'], transition=transition, user=self.request.user) 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()) return HttpResponseRedirect(self.get_success_url())
def get_form_kwargs(self): def get_form_kwargs(self):
@@ -136,14 +174,18 @@ class WorkflowInstanceTransitionView(FormView):
return get_object_or_404(WorkflowInstance, pk=self.kwargs['pk']) return get_object_or_404(WorkflowInstance, pk=self.kwargs['pk'])
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(WorkflowInstanceTransitionView, self).get_context_data(**kwargs) context = super(
WorkflowInstanceTransitionView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
'navigation_object_list': ['object', 'workflow_instance'], 'navigation_object_list': ['object', 'workflow_instance'],
'object': self.get_workflow_instance().document, 'object': self.get_workflow_instance().document,
'submit_label': _('Submit'), '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(), 'workflow_instance': self.get_workflow_instance(),
} }
) )
@@ -193,17 +235,21 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView):
def add(self, item): def add(self, item):
self.get_object().document_types.add(item) self.get_object().document_types.add(item)
# TODO: add task launching this workflow for all the document types of # TODO: add task launching this workflow for all the document types
# item # of item
def get_object(self): def get_object(self):
return get_object_or_404(Workflow, pk=self.kwargs['pk']) return get_object_or_404(Workflow, pk=self.kwargs['pk'])
def left_list(self): 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): 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): def remove(self, item):
self.get_object().document_types.remove(item) self.get_object().document_types.remove(item)
@@ -211,9 +257,13 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView):
# item # item
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super(SetupWorkflowDocumentTypesView, self).get_context_data(**kwargs) data = super(
SetupWorkflowDocumentTypesView, self
).get_context_data(**kwargs)
data.update({ 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(), 'object': self.get_object(),
}) })
@@ -223,11 +273,17 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView):
class SetupWorkflowStateListView(SingleObjectListView): class SetupWorkflowStateListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
Permission.check_permissions(request.user, [permission_workflow_edit]) Permission.check_permissions(
request.user, [permission_workflow_edit]
)
except PermissionDenied: 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): def get_workflow(self):
return get_object_or_404(Workflow, pk=self.kwargs['pk']) return get_object_or_404(Workflow, pk=self.kwargs['pk'])
@@ -236,7 +292,9 @@ class SetupWorkflowStateListView(SingleObjectListView):
return self.get_workflow().states.all() return self.get_workflow().states.all()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SetupWorkflowStateListView, self).get_context_data(**kwargs) context = super(
SetupWorkflowStateListView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
'hide_link': True, 'hide_link': True,
@@ -253,18 +311,28 @@ class SetupWorkflowStateCreateView(SingleObjectCreateView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
Permission.check_permissions(request.user, [permission_workflow_edit]) Permission.check_permissions(
request.user, [permission_workflow_edit]
)
except PermissionDenied: 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): def get_context_data(self, **kwargs):
context = super(SetupWorkflowStateCreateView, self).get_context_data(**kwargs) context = super(
SetupWorkflowStateCreateView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
'object': self.get_workflow(), 'object': self.get_workflow(),
'title': _('Create states for workflow: %s') % self.get_workflow() 'title': _(
'Create states for workflow: %s'
) % self.get_workflow()
} }
) )
return context return context
@@ -276,7 +344,9 @@ class SetupWorkflowStateCreateView(SingleObjectCreateView):
return self.get_workflow().states.all() return self.get_workflow().states.all()
def get_success_url(self): 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): def form_valid(self, form):
self.object = form.save(commit=False) self.object = form.save(commit=False)
@@ -290,7 +360,9 @@ class SetupWorkflowStateDeleteView(SingleObjectDeleteView):
view_permission = permission_workflow_delete view_permission = permission_workflow_delete
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SetupWorkflowStateDeleteView, self).get_context_data(**kwargs) context = super(
SetupWorkflowStateDeleteView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
@@ -303,7 +375,10 @@ class SetupWorkflowStateDeleteView(SingleObjectDeleteView):
return context return context
def get_success_url(self): 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): class SetupWorkflowStateEditView(SingleObjectEditView):
@@ -312,7 +387,9 @@ class SetupWorkflowStateEditView(SingleObjectEditView):
view_permission = permission_workflow_edit view_permission = permission_workflow_edit
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SetupWorkflowStateEditView, self).get_context_data(**kwargs) context = super(
SetupWorkflowStateEditView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
@@ -325,7 +402,10 @@ class SetupWorkflowStateEditView(SingleObjectEditView):
return context return context
def get_success_url(self): 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 # Transitions
@@ -334,11 +414,17 @@ class SetupWorkflowStateEditView(SingleObjectEditView):
class SetupWorkflowTransitionListView(SingleObjectListView): class SetupWorkflowTransitionListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
Permission.check_permissions(request.user, [permission_workflow_edit]) Permission.check_permissions(
request.user, [permission_workflow_edit]
)
except PermissionDenied: 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): def get_workflow(self):
return get_object_or_404(Workflow, pk=self.kwargs['pk']) return get_object_or_404(Workflow, pk=self.kwargs['pk'])
@@ -347,12 +433,16 @@ class SetupWorkflowTransitionListView(SingleObjectListView):
return self.get_workflow().transitions.all() return self.get_workflow().transitions.all()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SetupWorkflowTransitionListView, self).get_context_data(**kwargs) context = super(
SetupWorkflowTransitionListView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
'hide_link': True, 'hide_link': True,
'object': self.get_workflow(), '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): def dispatch(self, request, *args, **kwargs):
try: try:
Permission.check_permissions(request.user, [permission_workflow_edit]) Permission.check_permissions(
request.user, [permission_workflow_edit]
)
except PermissionDenied: 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): def get_context_data(self, **kwargs):
context = super(SetupWorkflowTransitionCreateView, self).get_context_data(**kwargs) context = super(
SetupWorkflowTransitionCreateView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
'object': self.get_workflow(), 'object': self.get_workflow(),
'title': _('Create transitions for workflow: %s') % self.get_workflow() 'title': _(
'Create transitions for workflow: %s'
) % self.get_workflow()
} }
) )
return context return context
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(SetupWorkflowTransitionCreateView, self).get_form_kwargs() kwargs = super(
SetupWorkflowTransitionCreateView, self
).get_form_kwargs()
kwargs['workflow'] = self.get_workflow() kwargs['workflow'] = self.get_workflow()
return kwargs return kwargs
@@ -392,7 +494,10 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView):
return self.get_workflow().transitions.all() return self.get_workflow().transitions.all()
def get_success_url(self): 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): def form_valid(self, form):
self.object = form.save(commit=False) self.object = form.save(commit=False)
@@ -400,8 +505,12 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView):
try: try:
self.object.save() self.object.save()
except IntegrityError: except IntegrityError:
messages.error(self.request, _('Unable to save transition; integrity error.')) messages.error(
return super(SetupWorkflowTransitionCreateView, self).form_invalid(form) self.request, _('Unable to save transition; integrity error.')
)
return super(
SetupWorkflowTransitionCreateView, self
).form_invalid(form)
else: else:
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
@@ -411,7 +520,9 @@ class SetupWorkflowTransitionDeleteView(SingleObjectDeleteView):
view_permission = permission_workflow_delete view_permission = permission_workflow_delete
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SetupWorkflowTransitionDeleteView, self).get_context_data(**kwargs) context = super(
SetupWorkflowTransitionDeleteView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
@@ -424,7 +535,10 @@ class SetupWorkflowTransitionDeleteView(SingleObjectDeleteView):
return context return context
def get_success_url(self): 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): class SetupWorkflowTransitionEditView(SingleObjectEditView):
@@ -433,7 +547,9 @@ class SetupWorkflowTransitionEditView(SingleObjectEditView):
view_permission = permission_workflow_edit view_permission = permission_workflow_edit
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SetupWorkflowTransitionEditView, self).get_context_data(**kwargs) context = super(
SetupWorkflowTransitionEditView, self
).get_context_data(**kwargs)
context.update( context.update(
{ {
@@ -446,9 +562,14 @@ class SetupWorkflowTransitionEditView(SingleObjectEditView):
return context return context
def get_form_kwargs(self): 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 kwargs['workflow'] = self.get_object().workflow
return kwargs return kwargs
def get_success_url(self): 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(): if serializer.is_valid():
# Nested resource we take the document pk from the URL and insert it # Nested resource we take the document pk from the URL and insert
# so that it needs not to be specified by the user, we mark it as # it so that it needs not to be specified by the user, we mark
# a read only field in the serializer # it as a read only field in the serializer
document = get_object_or_404(Document, pk=kwargs['pk']) document = get_object_or_404(Document, pk=kwargs['pk'])
document.new_version( document.new_version(
@@ -361,7 +361,8 @@ class APIDocumentTypeDocumentListView(generics.ListAPIView):
) )
except PermissionDenied: except PermissionDenied:
AccessControlList.objects.check_access( 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() return document_type.documents.all()

View File

@@ -84,8 +84,10 @@ class DocumentsApp(MayanAppConfig):
MissingItem( MissingItem(
label=_('Create a document type'), label=_('Create a document type'),
description=_('Every uploaded document must be assigned a document type, it is the basic way Mayan EDMS categorizes documents.'), description=_(
condition=lambda: not DocumentType.objects.exists(), '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='documents:document_type_list'
) )

View File

@@ -42,7 +42,9 @@ class DocumentPreviewForm(forms.Form):
super(DocumentPreviewForm, self).__init__(*args, **kwargs) super(DocumentPreviewForm, self).__init__(*args, **kwargs)
self.fields['preview'].initial = document self.fields['preview'].initial = document
try: try:
self.fields['preview'].label = _('Document pages (%d)') % document.page_count self.fields['preview'].label = _(
'Document pages (%d)'
) % document.page_count
except AttributeError: except AttributeError:
self.fields['preview'].label = _('Document pages (%d)') % 0 self.fields['preview'].label = _('Document pages (%d)') % 0
@@ -62,13 +64,16 @@ class DocumentForm(forms.ModelForm):
super(DocumentForm, self).__init__(*args, **kwargs) 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: if self.instance and self.instance.pk:
document_type = self.instance.document_type document_type = self.instance.document_type
filenames_qs = document_type.filenames.filter(enabled=True) filenames_qs = document_type.filenames.filter(enabled=True)
if filenames_qs.count(): if filenames_qs.count():
self.fields['document_type_available_filenames'] = forms.ModelChoiceField( self.fields[
'document_type_available_filenames'
] = forms.ModelChoiceField(
queryset=filenames_qs, queryset=filenames_qs,
required=False, required=False,
label=_('Quick document rename') label=_('Quick document rename')
@@ -119,12 +124,20 @@ class DocumentTypeFilenameForm_create(forms.ModelForm):
class DocumentDownloadForm(forms.Form): class DocumentDownloadForm(forms.Form):
compressed = forms.BooleanField( compressed = forms.BooleanField(
label=_('Compress'), required=False, 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( zip_filename = forms.CharField(
initial=DEFAULT_ZIP_FILENAME, label=_('Compressed filename'), initial=DEFAULT_ZIP_FILENAME, label=_('Compressed filename'),
required=False, 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): def __init__(self, *args, **kwargs):

View File

@@ -38,67 +38,229 @@ def is_min_zoom(context):
# Facet # Facet
link_document_preview = Link(permissions=[permission_document_view], text=_('Preview'), view='documents:document_preview', args='object.id') link_document_preview = Link(
link_document_properties = Link(permissions=[permission_document_view], text=_('Properties'), view='documents:document_properties', args='object.id') permissions=[permission_document_view], text=_('Preview'),
link_document_version_list = Link(permissions=[permission_document_view], text=_('Versions'), view='documents:document_version_list', args='object.pk') view='documents:document_preview', args='object.id'
link_document_pages = Link(permissions=[permission_document_view], text=_('Pages'), view='documents:document_pages', args='resolved_object.pk') )
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 # Actions
link_document_clear_transformations = Link(permissions=[permission_transformation_delete], text=_('Clear transformations'), view='documents:document_clear_transformations', args='object.id') link_document_clear_transformations = Link(
link_document_delete = Link(permissions=[permission_document_delete], tags='dangerous', text=_('Delete'), view='documents:document_delete', args='object.id') permissions=[permission_transformation_delete],
link_document_trash = Link(permissions=[permission_document_trash], tags='dangerous', text=_('Move to trash'), view='documents:document_trash', args='object.id') text=_('Clear transformations'),
link_document_edit = Link(permissions=[permission_document_properties_edit], text=_('Edit properties'), view='documents:document_edit', args='object.id') view='documents:document_clear_transformations', 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_delete = Link(
link_document_print = Link(permissions=[permission_document_print], text=_('Print'), view='documents:document_print', args='object.id') permissions=[permission_document_delete], tags='dangerous',
link_document_update_page_count = Link(permissions=[permission_document_tools], text=_('Reset page count'), view='documents:document_update_page_count', args='object.pk') text=_('Delete'), view='documents:document_delete', args='object.id'
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_trash = Link(
link_document_multiple_trash = Link(permissions=[permission_document_trash], tags='dangerous', text=_('Move to trash'), view='documents:document_multiple_trash') permissions=[permission_document_trash], tags='dangerous',
link_document_multiple_delete = Link(permissions=[permission_document_delete], tags='dangerous', text=_('Delete'), view='documents:document_multiple_delete') text=_('Move to trash'), view='documents:document_trash', args='object.id'
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_edit = Link(
link_document_multiple_update_page_count = Link(permissions=[permission_document_tools], text=_('Reset page count'), view='documents:document_multiple_update_page_count') permissions=[permission_document_properties_edit],
link_document_multiple_restore = Link(permissions=[permission_document_restore], text=_('Restore'), view='documents:document_multiple_restore') text=_('Edit properties'), view='documents:document_edit',
link_document_version_download = Link(args='object.pk', permissions=[permission_document_download], text=_('Download'), view='documents:document_version_download') 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 # Views
link_document_list = Link(icon='fa fa-file', text=_('All documents'), view='documents:document_list') link_document_list = Link(
link_document_list_recent = Link(icon='fa fa-clock-o', text=_('Recent documents'), view='documents:document_list_recent') icon='fa fa-file', text=_('All documents'), view='documents:document_list'
link_document_list_deleted = Link(icon='fa fa-trash', text=_('Trash'), view='documents:document_list_deleted') )
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 # Tools
link_clear_image_cache = Link( link_clear_image_cache = Link(
icon='fa fa-file-image-o', 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'), permissions=[permission_document_tools], text=_('Clear document cache'),
view='documents:document_clear_image_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 # 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_first = Link(
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') conditional_disable=is_first_page, icon='fa fa-step-backward',
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') keep_query=True, permissions=[permission_document_view],
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') text=_('First page'), view='documents:document_page_navigation_first',
link_document_page_return = Link(icon='fa fa-file', permissions=[permission_document_view], text=_('Document'), view='documents:document_preview', args='resolved_object.document.pk') args='resolved_object.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_navigation_last = Link(
link_document_page_view = Link(permissions=[permission_document_view], text=_('Page image'), view='documents:document_page_view', args='resolved_object.pk') conditional_disable=is_last_page, icon='fa fa-step-forward',
link_document_page_view_reset = Link(permissions=[permission_document_view], text=_('Reset view'), view='documents:document_page_view_reset', args='resolved_object.pk') keep_query=True, text=_('Last page'),
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') permissions=[permission_document_view],
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') 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 # 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 # 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_create = Link(
link_document_type_delete = Link(permissions=[permission_document_type_delete], tags='dangerous', text=_('Delete'), view='documents:document_type_delete', args='resolved_object.id') permissions=[permission_document_type_create],
link_document_type_edit = Link(permissions=[permission_document_type_edit], text=_('Edit'), view='documents:document_type_edit', args='resolved_object.id') text=_('Create document type'), view='documents:document_type_create'
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_delete = Link(
link_document_type_filename_edit = Link(permissions=[permission_document_type_edit], text=_('Edit'), view='documents:document_type_filename_edit', args='resolved_object.id') permissions=[permission_document_type_delete], tags='dangerous',
link_document_type_filename_list = Link(permissions=[permission_document_type_view], text=_('Filenames'), view='documents:document_type_filename_list', args='resolved_object.id') text=_('Delete'), view='documents:document_type_delete',
link_document_type_list = Link(permissions=[permission_document_type_view], text=_('Document types'), view='documents:document_type_list') args='resolved_object.id'
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_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): class RecentDocumentManager(models.Manager):
def add_document_for_user(self, user, document): def add_document_for_user(self, user, document):
if user.is_authenticated(): 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: if not created:
# document already in the recent list, just save to force # document already in the recent list, just save to force
# accessed date and time update # accessed date and time update
@@ -25,7 +27,9 @@ class RecentDocumentManager(models.Manager):
document_model = apps.get_model('documents', 'document') document_model = apps.get_model('documents', 'document')
if user.is_authenticated(): 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: else:
return document_model.objects.none() return document_model.objects.none()
@@ -37,7 +41,9 @@ class DocumentTypeManager(models.Manager):
class DocumentManager(models.Manager): class DocumentManager(models.Manager):
def get_queryset(self): 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): def invalidate_cache(self):
for document in self.model.objects.all(): for document in self.model.objects.all():
@@ -50,7 +56,9 @@ class PassthroughManager(models.Manager):
class TrashCanManager(models.Manager): class TrashCanManager(models.Manager):
def get_queryset(self): 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): class TrashCanQuerySet(models.QuerySet):

View File

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

View File

@@ -6,24 +6,61 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('documents', _('Documents')) namespace = PermissionNamespace('documents', _('Documents'))
permission_document_create = namespace.add_permission(name='document_create', label=_('Create documents')) permission_document_create = namespace.add_permission(
permission_document_delete = namespace.add_permission(name='document_delete', label=_('Delete documents')) name='document_create', label=_('Create 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_delete = namespace.add_permission(
permission_document_edit = namespace.add_permission(name='document_edit', label=_('Edit documents')) name='document_delete', label=_('Delete 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_trash = namespace.add_permission(
permission_document_print = namespace.add_permission(name='document_print', label=_('Can print documents')) name='document_trash', label=_('Trash 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_download = namespace.add_permission(
permission_document_version_revert = namespace.add_permission(name='document_version_revert', label=_('Revert documents to a previous version')) name='document_download', label=_('Download documents')
permission_document_view = namespace.add_permission(name='document_view', label=_('View 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_create = setup_namespace.add_permission(
permission_document_type_delete = setup_namespace.add_permission(name='document_type_delete', label=_('Delete document types')) name='document_type_create', label=_('Create 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_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 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(
document_search.add_model_field(field='versions__mimetype', label=_('MIME type')) 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='label', label=_('Label'))
document_search.add_model_field(field='description', label=_('Description')) 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) versions = DocumentVersionSerializer(many=True, read_only=True)
# TODO: Deprecate, move this as an entry point of DocumentVersion's pages # TODO: Deprecate, move this as an entry point of DocumentVersion's pages
image = serializers.HyperlinkedIdentityField(view_name='document-image') 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() document_type = DocumentTypeSerializer()
class Meta: 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 model = Document
@@ -53,7 +58,10 @@ class NewDocumentSerializer(serializers.Serializer):
document_type = serializers.IntegerField() document_type = serializers.IntegerField()
file = serializers.FileField() file = serializers.FileField()
label = serializers.CharField(required=False) 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): class RecentDocumentSerializer(serializers.ModelSerializer):

View File

@@ -7,21 +7,75 @@ from django.utils.translation import ugettext_lazy as _
from smart_settings import Namespace from smart_settings import Namespace
# TODO: Findout method to make languages names' translatable. # TODO: Findout method to make languages names' translatable.
# YAML fails to serialize ugettext_lazy and ugettext is not allowed at this level # YAML fails to serialize ugettext_lazy and ugettext is not allowed at this
LANGUAGE_CHOICES = [(i.terminology, i.name) for i in list(pycountry.languages)] # level
LANGUAGE_CHOICES = [
(i.terminology, i.name) for i in list(pycountry.languages)
]
namespace = Namespace(name='documents', label=_('Documents')) namespace = Namespace(name='documents', label=_('Documents'))
setting_storage_backend = namespace.add_setting(global_name='DOCUMENTS_STORAGE_BACKEND', default='storage.backends.filebasedstorage.FileBasedStorage') setting_storage_backend = namespace.add_setting(
setting_preview_size = namespace.add_setting(global_name='DOCUMENTS_PREVIEW_SIZE', default='640x480') global_name='DOCUMENTS_STORAGE_BACKEND',
setting_print_size = namespace.add_setting(global_name='DOCUMENTS_PRINT_SIZE', default='3600') default='storage.backends.filebasedstorage.FileBasedStorage'
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_preview_size = namespace.add_setting(
setting_display_size = namespace.add_setting(global_name='DOCUMENTS_DISPLAY_SIZE', default='3600') global_name='DOCUMENTS_PREVIEW_SIZE', default='640x480'
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_print_size = namespace.add_setting(
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.')) global_name='DOCUMENTS_PRINT_SIZE', default='3600'
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_multipage_preview_size = namespace.add_setting(
setting_cache_storage_backend = namespace.add_setting(global_name='DOCUMENTS_CACHE_STORAGE_BACKEND', default='documents.storage.LocalCacheFileStorage') global_name='DOCUMENTS_MULTIPAGE_PREVIEW_SIZE', default='160x120'
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_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 from django.dispatch import Signal
post_version_upload = Signal(providing_args=['instance'], use_caching=True) 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) 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 total_size = 0
for filename in file_list: for filename in file_list:
try: 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: except OSError:
pass pass
@@ -45,11 +47,19 @@ class DocumentStatistics(Statistic):
results.extend([ results.extend([
_('Document types: %d') % DocumentType.objects.count(), _('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([ 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), 'Minimum amount of pages per document: %d'
_('Average amount of pages per document: %f') % (document_stats['page_count__avg'] or 0), ) % (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 return results
@@ -69,16 +79,25 @@ class DocumentUsageStatistics(Statistic):
total_storage_documents, storage_used_space = storage_count() total_storage_documents, storage_used_space = storage_count()
results.append(_('Documents in storage: %d') % results.append(_('Documents in storage: %d') %
total_storage_documents) total_storage_documents)
results.append(_('Space used in storage: %(base_2)s (base 2), %(base_10)s (base 10), %(bytes)d bytes') % { results.append(
'base_2': pretty_size(storage_used_space), _(
'base_10': pretty_size_10(storage_used_space), 'Space used in storage: %(base_2)s (base 2), %(base_10)s '
'bytes': storage_used_space '(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: except NotImplementedError:
pass pass
results.extend([ results.extend(
_('Document pages in database: %d') % DocumentPage.objects.only('pk',).count(), [
]) _(
'Document pages in database: %d'
) % DocumentPage.objects.only('pk',).count(),
]
)
return results return results

View File

@@ -27,16 +27,31 @@ def task_check_delete_periods():
logger.info('Executing') logger.info('Executing')
for document_type in DocumentType.objects.all(): 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: if document_type.delete_time_period and document_type.delete_time_unit:
delta = timedelta(**{document_type.delete_time_unit: document_type.delete_time_period}) delta = timedelta(
logger.info('Document type: %s, has a deletion period delta of: %s', document_type, delta) **{
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): for document in DeletedDocument.objects.filter(document_type=document_type):
if now() > document.deleted_date_time + delta: 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() document.delete()
else: 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') logger.info('Finshed')
@@ -46,16 +61,31 @@ def task_check_trash_periods():
logger.info('Executing') logger.info('Executing')
for document_type in DocumentType.objects.all(): 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: if document_type.trash_time_period and document_type.trash_time_unit:
delta = timedelta(**{document_type.trash_time_unit: document_type.trash_time_period}) delta = timedelta(
logger.info('Document type: %s, has a trash period delta of: %s', document_type, delta) **{
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): for document in Document.objects.filter(document_type=document_type):
if now() > document.date_added + delta: 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() document.delete()
else: 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') logger.info('Finshed')
@@ -80,7 +110,11 @@ def task_update_page_count(self, version_id):
try: try:
document_version.update_page_count() document_version.update_page_count()
except OperationalError as exception: 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) 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): def task_upload_new_document(self, document_type_id, shared_uploaded_file_id, description=None, label=None, language=None, user_id=None):
try: try:
document_type = DocumentType.objects.get(pk=document_type_id) 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: if user_id:
user = User.objects.get(pk=user_id) user = User.objects.get(pk=user_id)
else: else:
user = None user = None
except OperationalError as exception: 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) raise self.retry(exc=exception)
try: try:
with shared_file.open as file_object: 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: 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) raise self.retry(exc=exception)
try: try:
shared_file.delete() shared_file.delete()
except OperationalError as exception: 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) @app.task(bind=True, default_retry_delay=UPLOAD_NEW_VERSION_RETRY_DELAY, ignore_result=True)
def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, comment=None): def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, comment=None):
try: try:
document = Document.objects.get(pk=document_id) 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: if user_id:
user = User.objects.get(pk=user_id) user = User.objects.get(pk=user_id)
else: else:
user = None user = None
except OperationalError as exception: 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) raise self.retry(exc=exception)
with shared_file.open() as file_object: 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: try:
document_version.save(_user=user) document_version.save(_user=user)
except Warning as warning: except Warning as warning:
# New document version are blocked # 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() shared_file.delete()
except OperationalError as exception: 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) raise self.retry(exc=exception)
except Exception as exception: except Exception as exception:
# This except and else block emulate a finally: # 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: try:
shared_file.delete() shared_file.delete()
except OperationalError as exception: 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: else:
try: try:
shared_file.delete() shared_file.delete()
except OperationalError as exception: 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): class DocumentAPICreateDocumentTestCase(TestCase):
""" """
Functional test to make sure all the moving parts to create a document from Functional test to make sure all the moving parts to create a document
the API are working correctly from the API are working correctly
""" """
def setUp(self): 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(
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE) 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 = self.document_type.ocr_settings
ocr_settings.auto_ocr = False ocr_settings.auto_ocr = False
@@ -41,7 +46,12 @@ class DocumentAPICreateDocumentTestCase(TestCase):
def test_uploading_a_document_using_token_auth(self): def test_uploading_a_document_using_token_auth(self):
# Get the an user token # Get the an user token
token_client = APIClient() 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 # Be able to get authentication token
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -66,17 +76,26 @@ class DocumentAPICreateDocumentTestCase(TestCase):
# Create a blank document # Create a blank document
with open(TEST_SMALL_DOCUMENT_PATH) as file_descriptor: 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) self.assertEqual(document_response.status_code, status.HTTP_201_CREATED)
# The document was created in the DB? # The document was created in the DB?
self.assertEqual(Document.objects.count(), 1) 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: 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 # Make sure the document uploaded correctly
document = Document.objects.first() document = Document.objects.first()
@@ -86,13 +105,20 @@ class DocumentAPICreateDocumentTestCase(TestCase):
self.assertEqual(document.file_mimetype, 'application/pdf') self.assertEqual(document.file_mimetype, 'application/pdf')
self.assertEqual(document.file_mime_encoding, 'binary') self.assertEqual(document.file_mime_encoding, 'binary')
self.assertEqual(document.label, TEST_SMALL_DOCUMENT_FILENAME) self.assertEqual(document.label, TEST_SMALL_DOCUMENT_FILENAME)
self.assertEqual(document.checksum, 'c637ffab6b8bb026ed3784afdb07663fddc60099853fae2be93890852a69ecf3') self.assertEqual(
document.checksum,
'c637ffab6b8bb026ed3784afdb07663fddc60099853fae2be93890852a69ecf3'
)
self.assertEqual(document.page_count, 47) self.assertEqual(document.page_count, 47)
# Make sure we can edit the document via the API # 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') # 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_SMALL_DOCUMENT_FILENAME = 'title_page.png'
TEST_NON_ASCII_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png' TEST_NON_ASCII_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png'
TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png.zip' 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_DOCUMENT_PATH = os.path.join(
TEST_SMALL_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_SMALL_DOCUMENT_FILENAME) settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf'
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_SMALL_DOCUMENT_PATH = os.path.join(
TEST_DEU_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_DEU_DOCUMENT_FILENAME) settings.BASE_DIR, 'contrib', 'sample_documents',
TEST_COMPRESSED_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_COMPRESSED_DOCUMENTS_FILENAME) 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_DESCRIPTION = 'test description'
TEST_DOCUMENT_TYPE = 'test_document_type' TEST_DOCUMENT_TYPE = 'test_document_type'
class DocumentTestCase(TestCase): class DocumentTestCase(TestCase):
def setUp(self): 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 = self.document_type.ocr_settings
ocr_settings.auto_ocr = False ocr_settings.auto_ocr = False
ocr_settings.save() ocr_settings.save()
with open(TEST_DOCUMENT_PATH) as file_object: 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): def tearDown(self):
self.document_type.delete() self.document_type.delete()
@@ -50,7 +71,10 @@ class DocumentTestCase(TestCase):
self.assertEqual(self.document.file_mimetype, 'application/pdf') self.assertEqual(self.document.file_mimetype, 'application/pdf')
self.assertEqual(self.document.file_mime_encoding, 'binary') self.assertEqual(self.document.file_mime_encoding, 'binary')
self.assertEqual(self.document.label, 'mayan_11_1.pdf') 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) self.assertEqual(self.document.page_count, 47)
def test_version_creation(self): def test_version_creation(self):
@@ -58,7 +82,9 @@ class DocumentTestCase(TestCase):
self.document.new_version(file_object=File(file_object)) self.document.new_version(file_object=File(file_object))
with open(TEST_SMALL_DOCUMENT_PATH) as 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) self.assertEqual(self.document.versions.count(), 3)

View File

@@ -22,20 +22,29 @@ class DocumentsViewsFunctionalTestCase(TestCase):
""" """
def setUp(self): 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 = self.document_type.ocr_settings
ocr_settings.auto_ocr = False ocr_settings.auto_ocr = False
ocr_settings.save() 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() self.client = Client()
# Login the admin user # 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(logged_in)
self.assertTrue(self.admin_user.is_authenticated()) self.assertTrue(self.admin_user.is_authenticated())
with open(TEST_SMALL_DOCUMENT_PATH) as file_object: 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): def tearDown(self):
self.document_type.delete() self.document_type.delete()
@@ -45,12 +54,16 @@ class DocumentsViewsFunctionalTestCase(TestCase):
self.assertEqual(Document.objects.count(), 1) self.assertEqual(Document.objects.count(), 1)
# Trash the document # 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(DeletedDocument.objects.count(), 1)
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
# Restore the document # 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(DeletedDocument.objects.count(), 0)
self.assertEqual(Document.objects.count(), 1) self.assertEqual(Document.objects.count(), 1)
@@ -58,12 +71,16 @@ class DocumentsViewsFunctionalTestCase(TestCase):
self.assertEqual(Document.objects.count(), 1) self.assertEqual(Document.objects.count(), 1)
# Trash the document # 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(DeletedDocument.objects.count(), 1)
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
# Delete the document # 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(DeletedDocument.objects.count(), 0)
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
@@ -72,8 +89,12 @@ class DocumentsViewsFunctionalTestCase(TestCase):
self.assertContains(response, 'Total: 1', status_code=200) self.assertContains(response, 'Total: 1', status_code=200)
# test document simple view # test document simple view
response = self.client.get(reverse('documents:document_properties', args=[self.document.pk])) response = self.client.get(
self.assertContains(response, 'roperties for document', status_code=200) reverse('documents:document_properties', args=[self.document.pk])
)
self.assertContains(
response, 'roperties for document', status_code=200
)
def test_document_type_views(self): def test_document_type_views(self):
# Check that there are no document types # Check that there are no document types
@@ -81,8 +102,12 @@ class DocumentsViewsFunctionalTestCase(TestCase):
self.assertContains(response, 'Total: 1', status_code=200) self.assertContains(response, 'Total: 1', status_code=200)
# Create a document type # Create a document type
response = self.client.post(reverse('documents:document_type_create'), {'name': 'test document type 2'}, follow=True) response = self.client.post(
#TODO: FIX self.assertContains(response, 'successfully', status_code=200) 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 # Check that there are two document types
response = self.client.get(reverse('documents:document_type_list')) 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) self.assertEqual(self.document_type.label, TEST_DOCUMENT_TYPE)
# Edit the 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) response = self.client.post(
#TODO: FIX self.assertContains(response, 'Document type edited successfully', status_code=200) 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 # Reload document type model data
#self.document_type = DocumentType.objects.get(pk=self.document_type.pk) #self.document_type = DocumentType.objects.get(
#TODO: FIX self.assertEqual(self.document_type.name, TEST_DOCUMENT_TYPE + 'partial') # pk=self.document_type.pk
#)
#TODO: FIX#
#self.assertEqual(
# self.document_type.name, TEST_DOCUMENT_TYPE + 'partial'
#)
# Delete the document type # Delete the document type
response = self.client.post(reverse('documents:document_type_delete', args=[self.document_type.pk]), follow=True) #response = self.client.post(
#TODO: FIX self.assertContains(response, 'Document type: {0} deleted successfully'.format(self.document_type.name), status_code=200) # 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 # 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.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( urlpatterns = patterns(
'documents.views', 'documents.views',
url(r'^list/$', DocumentListView.as_view(), name='document_list'), url(r'^list/$', DocumentListView.as_view(), name='document_list'),
url(r'^list/recent/$', RecentDocumentListView.as_view(), name='document_list_recent'), url(
url(r'^list/deleted/$', DeletedDocumentListView.as_view(), name='document_list_deleted'), 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(
url(r'^(?P<document_id>\d+)/properties/$', 'document_properties', name='document_properties'), r'^(?P<document_id>\d+)/preview/$', 'document_preview',
url(r'^(?P<pk>\d+)/restore/$', DocumentRestoreView.as_view(), name='document_restore'), name='document_preview'
url(r'^multiple/restore/$', DocumentManyRestoreView.as_view(), name='document_multiple_restore'), ),
url(r'^(?P<pk>\d+)/delete/$', DeletedDocumentDeleteView.as_view(), name='document_delete'), url(
url(r'^multiple/delete/$', DocumentManyDeleteView.as_view(), name='document_multiple_delete'), r'^(?P<document_id>\d+)/properties/$', 'document_properties',
url(r'^(?P<document_id>\d+)/type/$', 'document_document_type_edit', name='document_document_type_edit'), name='document_properties'
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(
url(r'^multiple/trash/$', 'document_multiple_trash', name='document_multiple_trash'), r'^(?P<pk>\d+)/restore/$', DocumentRestoreView.as_view(),
url(r'^(?P<document_id>\d+)/edit/$', 'document_edit', name='document_edit'), name='document_restore'
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(
url(r'^multiple/reset_page_count/$', 'document_multiple_update_page_count', name='document_multiple_update_page_count'), 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(
url(r'^(?P<document_id>\d+)/display/print/$', 'get_document_image', {'size': setting_print_size.value}, 'document_display_print'), 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(
url(r'^multiple/download/$', 'document_multiple_download', name='document_multiple_download'), r'^(?P<document_id>\d+)/download/$', 'document_download',
url(r'^(?P<document_id>\d+)/clear_transformations/$', 'document_clear_transformations', name='document_clear_transformations'), 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(
url(r'^document/version/(?P<document_version_pk>\d+)/download/$', 'document_download', name='document_version_download'), r'^(?P<document_pk>\d+)/version/all/$', 'document_version_list',
url(r'^document/version/(?P<document_version_pk>\d+)/revert/$', 'document_version_revert', name='document_version_revert'), 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(
url(r'^cache/clear/$', 'document_clear_image_cache', name='document_clear_image_cache'), r'^multiple/clear_transformations/$',
url(r'^trash_can/empty/$', EmptyTrashCanView.as_view(), name='trash_can_empty'), '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(
url(r'^page/(?P<document_page_id>\d+)/navigation/next/$', 'document_page_navigation_next', name='document_page_navigation_next'), r'^page/(?P<document_page_id>\d+)/$', 'document_page_view',
url(r'^page/(?P<document_page_id>\d+)/navigation/previous/$', 'document_page_navigation_previous', name='document_page_navigation_previous'), name='document_page_view'
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(
url(r'^page/(?P<document_page_id>\d+)/zoom/in/$', 'document_page_zoom_in', name='document_page_zoom_in'), r'^page/(?P<document_page_id>\d+)/navigation/next/$',
url(r'^page/(?P<document_page_id>\d+)/zoom/out/$', 'document_page_zoom_out', name='document_page_zoom_out'), 'document_page_navigation_next', name='document_page_navigation_next'
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(
url(r'^page/(?P<document_page_id>\d+)/reset/$', 'document_page_view_reset', name='document_page_view_reset'), 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 # Admin views
url(r'^type/list/$', DocumentTypeListView.as_view(), name='document_type_list'), url(
url(r'^type/create/$', DocumentTypeCreateView.as_view(), name='document_type_create'), r'^type/list/$', DocumentTypeListView.as_view(),
url(r'^type/(?P<pk>\d+)/edit/$', DocumentTypeEditView.as_view(), name='document_type_edit'), name='document_type_list'
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(
url(r'^type/(?P<document_type_id>\d+)/filename/list/$', 'document_type_filename_list', name='document_type_filename_list'), r'^type/create/$', DocumentTypeCreateView.as_view(),
url(r'^type/filename/(?P<document_type_filename_id>\d+)/edit/$', 'document_type_filename_edit', name='document_type_filename_edit'), name='document_type_create'
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/(?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( api_urls = patterns(
'', '',
url(r'^documents/$', APIDocumentListView.as_view(), name='document-list'), url(r'^documents/$', APIDocumentListView.as_view(), name='document-list'),
url(r'^documents/recent/$', APIRecentDocumentListView.as_view(), name='document-recent-list'), url(
url(r'^documents/(?P<pk>[0-9]+)/$', APIDocumentView.as_view(), name='document-detail'), r'^documents/recent/$', APIRecentDocumentListView.as_view(),
url(r'^document_version/(?P<pk>[0-9]+)/$', APIDocumentVersionView.as_view(), name='documentversion-detail'), name='document-recent-list'
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(
url(r'^documents/(?P<pk>[0-9]+)/new_version/$', APIDocumentVersionCreateView.as_view(), name='document-new-version'), r'^documents/(?P<pk>[0-9]+)/$', APIDocumentView.as_view(),
url(r'^documenttypes/(?P<pk>[0-9]+)/documents/$', APIDocumentTypeDocumentListView.as_view(), name='documenttype-document-list'), name='document-detail'
url(r'^documenttypes/(?P<pk>[0-9]+)/$', APIDocumentTypeView.as_view(), name='documenttype-detail'), ),
url(r'^documenttypes/$', APIDocumentTypeListView.as_view(), name='documenttype-list'), 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): 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() result = set()
for part in astr.split(','): for part in astr.split(','):
x = part.split('-') x = part.split('-')

View File

@@ -41,8 +41,8 @@ from .forms import (
) )
from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT
from .models import ( from .models import (
DeletedDocument, Document, DocumentType, DocumentPage, DocumentTypeFilename, DeletedDocument, Document, DocumentType, DocumentPage,
DocumentVersion, RecentDocument DocumentTypeFilename, DocumentVersion, RecentDocument
) )
from .permissions import ( from .permissions import (
permission_document_delete, permission_document_download, permission_document_delete, permission_document_download,
@@ -94,11 +94,17 @@ class DeletedDocumentListView(DocumentListView):
queryset = Document.trash.all() queryset = Document.trash.all()
try: try:
Permission.check_permissions(self.request.user, [permission_document_view]) Permission.check_permissions(
self.request.user, [permission_document_view]
)
except PermissionDenied: 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): class DeletedDocumentDeleteView(ConfirmView):
@@ -107,12 +113,18 @@ class DeletedDocumentDeleteView(ConfirmView):
} }
def object_action(self, request, instance): 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: try:
Permission.check_permissions(request.user, [permission_document_delete]) Permission.check_permissions(
request.user, [permission_document_delete]
)
except PermissionDenied: 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() instance.delete()
messages.success(request, _('Document: %(document)s deleted.') % { messages.success(request, _('Document: %(document)s deleted.') % {
@@ -132,16 +144,24 @@ class DocumentRestoreView(ConfirmView):
} }
def object_action(self, request, instance): 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: try:
Permission.check_permissions(request.user, [permission_document_restore]) Permission.check_permissions(
request.user, [permission_document_restore]
)
except PermissionDenied: 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() instance.restore()
messages.success(request, _('Document: %(document)s restored.') % { messages.success(
'document': instance} request, _('Document: %(document)s restored.') % {
'document': instance
}
) )
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -204,7 +224,9 @@ class EmptyTrashCanView(ConfirmView):
'title': _('Empty trash?') 'title': _('Empty trash?')
} }
view_permission = permission_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): def post(self, request, *args, **kwargs):
for deleted_document in DeletedDocument.objects.all(): for deleted_document in DeletedDocument.objects.all():
@@ -231,27 +253,43 @@ def document_properties(request, document_id):
try: try:
Permission.check_permissions(request.user, [permission_document_view]) Permission.check_permissions(request.user, [permission_document_view])
except PermissionDenied: 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.add_as_recent_document_for_user(request.user)
document_fields = [ document_fields = [
{'label': _('Date added'), 'field': lambda x: x.date_added.date()}, {'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'}, {'label': _('UUID'), 'field': 'uuid'},
] ]
if document.latest_version: if document.latest_version:
document_fields.extend([ 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 mimetype'),
{'label': _('File size'), 'field': lambda x: pretty_size(x.size) if x.size else '-'}, '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': _('Exists in storage'), 'field': 'exists'},
{'label': _('File path in storage'), 'field': 'file'}, {'label': _('File path in storage'), 'field': 'file'},
{'label': _('Checksum'), 'field': 'checksum'}, {'label': _('Checksum'), 'field': 'checksum'},
{'label': _('Pages'), 'field': 'page_count'}, {'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', { return render_to_response('appearance/generic_form.html', {
'form': document_properties_form, 'form': document_properties_form,
@@ -268,7 +306,9 @@ def document_preview(request, document_id):
try: try:
Permission.check_permissions(request.user, [permission_document_view]) Permission.check_permissions(request.user, [permission_document_view])
except PermissionDenied: 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.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)] documents = [get_object_or_404(Document, pk=document_id)]
post_action_redirect = reverse('documents:document_list_recent') post_action_redirect = reverse('documents:document_list_recent')
elif document_id_list: 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: else:
messages.error(request, _('Must provide at least one document.')) 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: try:
Permission.check_permissions(request.user, [permission_document_trash]) Permission.check_permissions(request.user, [permission_document_trash])
except PermissionDenied: 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)))) 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)))) 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) rotation = final_attrs.get('rotation', 0)
if value: if value:
output = [] output = []
output.append('<div class="full-height scrollable mayan-page-wrapper-interactive" data-height-difference=230>') output.append(
output.append(document_html_widget(value, zoom=zoom, rotation=rotation, image_class='lazy-load', nolazyload=False, size=setting_display_size.value)) '<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>') output.append('</div>')
return mark_safe(''.join(output)) return mark_safe(''.join(output))
else: else:
@@ -35,7 +41,10 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget):
""" """
def render(self, name, value, attrs=None): def render(self, name, value, attrs=None):
output = [] 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: try:
document_pages = value.pages.all() document_pages = value.pages.all()
@@ -57,7 +66,14 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget):
post_load_class='lazy-load-carousel-loaded', 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>')
output.append('</div>') output.append('</div>')
@@ -66,11 +82,16 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget):
def document_thumbnail(document, **kwargs): 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): 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): 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) 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: if title:
title_template = 'title="%s"' % strip_tags(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 = '' title_template = ''
if click_view: if click_view:
result.append('<a {gallery_template} class="{fancybox_class}" href="{image_data}" {title_template}>'.format( result.append(
gallery_template=gallery_template, '<a {gallery_template} class="{fancybox_class}" '
fancybox_class=fancybox_class, 'href="{image_data}" {title_template}>'.format(
image_data='%s?%s' % (reverse(click_view, args=click_view_arguments or [document.pk]), query_string), gallery_template=gallery_template,
title_template=title_template fancybox_class=fancybox_class,
)) image_data='%s?%s' % (
reverse(
click_view, args=click_view_arguments or [document.pk]
), query_string
),
title_template=title_template
)
)
if nolazyload: 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: 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: if click_view:
result.append('</a>') result.append('</a>')

View File

@@ -19,5 +19,12 @@ class DynamicSearchApp(MayanAppConfig):
APIEndPoint('search', app_name='dynamic_search') 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_facet.bind_links(
menu_sidebar.bind_links(links=[link_search_again], sources=['search:results']) 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, findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
normspace=re.compile(r'\s{2,}').sub): normspace=re.compile(r'\s{2,}').sub):
""" """
Splits the query string in invidual keywords, getting rid of unecessary spaces Splits the query string in invidual keywords, getting rid of
and grouping quoted words together. unecessary spaces and grouping quoted words together.
Example: Example:
>>> normalize_query(' some random words "with quotes " and spaces') >>> normalize_query(' some random words "with quotes " and spaces')
['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): def search(self, query_string, user, global_and_search=False):
elapsed_time = 0 elapsed_time = 0
@@ -102,7 +104,9 @@ class SearchModel(object):
search_dict[search_field.get_model()]['searches'].append( search_dict[search_field.get_model()]['searches'].append(
{ {
'field_name': [search_field.field], 'field_name': [search_field.field],
'terms': self.normalize_query(query_string.get('q', '').strip()) 'terms': self.normalize_query(
query_string.get('q', '').strip()
)
} }
) )
else: else:
@@ -117,7 +121,9 @@ class SearchModel(object):
search_dict[search_field.get_model()]['searches'].append( search_dict[search_field.get_model()]['searches'].append(
{ {
'field_name': [search_field.field], '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']: for query_entry in data['searches']:
# Fashion a list of queries for a field for each term # 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) logger.debug('field_query_list: %s', field_query_list)
@@ -139,20 +147,26 @@ class SearchModel(object):
# Get results per search field # Get results per search field
for query in field_query_list: for query in field_query_list:
logger.debug('query: %s', query) 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 # Convert the QuerySet to a Python set and perform the
# AND operation on the program and not as a query. # AND operation on the program and not as a query.
# This operation ANDs all the field term results # This operation ANDs all the field term results
# belonging to a single model, making sure to only include # belonging to a single model, making sure to only include
# results in the final field result variable if all the terms # results in the final field result variable if all the
# are found in a single field. # terms are found in a single field.
if not field_result_set: if not field_result_set:
field_result_set = term_query_result_set field_result_set = term_query_result_set
else: else:
field_result_set &= term_query_result_set 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) logger.debug('field_result_set: %s', field_result_set)
if global_and_search: if global_and_search:
@@ -165,24 +179,33 @@ class SearchModel(object):
result_set = result_set | model_result_set 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: if self.permission:
try: try:
Permission.check_permissions(user, [self.permission]) Permission.check_permissions(user, [self.permission])
except PermissionDenied: 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 return queryset, result_set, elapsed_time
def assemble_query(self, terms, search_fields): def assemble_query(self, terms, search_fields):
""" """
Returns a query, that is a combination of Q objects. That combination 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 = [] queries = []
for term in terms: for term in terms:

View File

@@ -5,5 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from navigation import Link from navigation import Link
link_search = Link(text=_('Search'), view='search:search') 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') 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): class RecentSearchManager(models.Manager):
def add_query_for_user(self, user, query_string, hits): 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(): for key, value in parsed_query.items():
parsed_query[key] = ' '.join(value) parsed_query[key] = ' '.join(value)
@@ -26,7 +28,10 @@ class RecentSearchManager(models.Manager):
if parsed_query and not isinstance(user, AnonymousUser): if parsed_query and not isinstance(user, AnonymousUser):
# If the URL query has at least one variable with a value # 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: if not created:
new_recent.hits = hits new_recent.hits = hits
new_recent.save() new_recent.save()

View File

@@ -6,7 +6,9 @@ import urlparse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models 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 django.utils.translation import ugettext_lazy as _
from .managers import RecentSearchManager from .managers import RecentSearchManager
@@ -24,13 +26,16 @@ class RecentSearch(models.Model):
# TODO: Fix after upgrade to DRF v2.4.4 # TODO: Fix after upgrade to DRF v2.4.4
query = models.TextField(editable=False, verbose_name=_('Query')) 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')) hits = models.IntegerField(editable=False, verbose_name=_('Hits'))
objects = RecentSearchManager() objects = RecentSearchManager()
def __str__(self): 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 from .classes import SearchModel
document_search = SearchModel.get('documents.Document') document_search = SearchModel.get('documents.Document')
@@ -41,7 +46,11 @@ class RecentSearch(models.Model):
advanced_string = [] advanced_string = []
for key, value in query_dict.items(): for key, value in query_dict.items():
search_field = document_search.get_search_field(key) 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) display_string = ', '.join(advanced_string)
else: else:

View File

@@ -6,6 +6,14 @@ from smart_settings import Namespace
namespace = Namespace(name='dynamic_search', label=_('Search')) namespace = Namespace(name='dynamic_search', label=_('Search'))
setting_show_object_type = namespace.add_setting(global_name='SEARCH_SHOW_OBJECT_TYPE', default=False) setting_show_object_type = namespace.add_setting(
setting_limit = namespace.add_setting(global_name='SEARCH_LIMIT', default=100, help_text=_('Maximum amount search hits to fetch and display.')) global_name='SEARCH_SHOW_OBJECT_TYPE', default=False
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_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): class DocumentSearchTestCase(TestCase):
def setUp(self): 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(
self.document_type = DocumentType.objects.create(label=TEST_DOCUMENT_TYPE) 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: 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): def tearDown(self):
self.document.delete() self.document.delete()
@@ -30,17 +37,24 @@ class DocumentSearchTestCase(TestCase):
document versions and document version pages 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(len(result_set), 1)
self.assertEqual(list(model_list), [self.document]) self.assertEqual(list(model_list), [self.document])
def test_advanced_search_after_related_name_change(self): def test_advanced_search_after_related_name_change(self):
# Test versions__filename # 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(len(result_set), 1)
self.assertEqual(list(model_list), [self.document]) self.assertEqual(list(model_list), [self.document])
# Test versions__mimetype # 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(len(result_set), 1)
self.assertEqual(list(model_list), [self.document]) self.assertEqual(list(model_list), [self.document])

View File

@@ -22,16 +22,23 @@ class Issue46TestCase(TestCase):
""" """
def setUp(self): 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() self.client = Client()
# Login the admin user # 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(logged_in)
self.assertTrue(self.admin_user.is_authenticated()) self.assertTrue(self.admin_user.is_authenticated())
self.document_count = 4 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 # Upload many instances of the same test document
for i in range(self.document_count): for i in range(self.document_count):
@@ -49,16 +56,28 @@ class Issue46TestCase(TestCase):
def test_advanced_search_past_first_page(self): def test_advanced_search_past_first_page(self):
# Make sure all documents are returned by the search # 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) self.assertEqual(len(result_set), self.document_count)
with self.settings(PAGINATION_DEFAULT_PAGINATION=2): with self.settings(PAGINATION_DEFAULT_PAGINATION=2):
reload(pagination_tags) reload(pagination_tags)
# Funcitonal test for the first page of advanced results # Funcitonal test for the first page of advanced results
response = self.client.get(reverse('search:results'), {'label': 'test'}) response = self.client.get(
self.assertContains(response, 'Total (1 - 2 out of 4) (Page 1 of 2)', status_code=200) 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 # Functional test for the second page of advanced results
response = self.client.get(reverse('search:results'), {'label': 'test', 'page': 2}) response = self.client.get(
self.assertContains(response, 'Total (3 - 4 out of 4) (Page 2 of 2)', status_code=200) 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( api_urls = patterns(
'', '',
url(r'^recent_searches/$', APIRecentSearchListView.as_view(), name='recentsearch-list'), url(
url(r'^recent_searches/(?P<pk>[0-9]+)/$', APIRecentSearchView.as_view(), name='recentsearch-detail'), 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'), 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: if setting_show_object_type.value:
context.update({ 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, return render_to_response(
context_instance=RequestContext(request)) 'dynamic_search/search_results.html', context,
context_instance=RequestContext(request)
)
def search(request, advanced=False): def search(request, advanced=False):
document_search = SearchModel.get('documents.Document') document_search = SearchModel.get('documents.Document')
if advanced: if advanced:
form = AdvancedSearchForm(data=request.GET, search_model=document_search) form = AdvancedSearchForm(
data=request.GET, search_model=document_search
)
return render_to_response( return render_to_response(
'appearance/generic_form.html', 'appearance/generic_form.html',
{ {
@@ -87,5 +96,9 @@ def search(request, advanced=False):
def search_again(request): def search_again(request):
query = urlparse.urlparse(request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))).query query = urlparse.urlparse(
return HttpResponseRedirect('%s?%s' % (reverse('search:search_advanced'), query)) 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=_('Timestamp'), attribute='timestamp')
SourceColumn(source=Action, label=_('Actor'), attribute='actor') 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]) menu_tools.bind_links(links=[link_events_list])

View File

@@ -28,4 +28,7 @@ class Event(object):
if not self.event_type: if not self.event_type:
self.event_type, created = model.objects.get_or_create(name=self.name) 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_factory(variable_name):
def get_kwargs(context): def get_kwargs(context):
content_type = ContentType.objects.get_for_model(context[variable_name]) 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 return get_kwargs
link_events_list = Link(icon='fa fa-list-ol', permissions=[permission_events_view], text=_('Events'), view='events:events_list') link_events_list = Link(
link_events_for_object = Link(permissions=[permission_events_view], text=_('Events'), view='events:events_for_object', kwargs=get_kwargs_factory('resolved_object')) 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 @python_2_unicode_compatible
class EventType(models.Model): 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): def __str__(self):
return unicode(Event.get_label(self.name)) 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 from permissions import PermissionNamespace
namespace = PermissionNamespace('events', _('Events')) 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( urlpatterns = patterns(
'events.views', 'events.views',
url(r'^all/$', EventListView.as_view(), name='events_list'), 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(
url(r'^by_verb/(?P<verb>[\w\-]+)/$', VerbEventListView.as_view(), name='events_by_verb'), 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': [ 'extra_columns': [
{ {
'name': _('Target'), 'name': _('Target'),
'attribute': encapsulate(lambda entry: event_object_link(entry)) 'attribute': encapsulate(
lambda entry: event_object_link(entry)
)
} }
], ],
'hide_object': True, 'hide_object': True,
@@ -41,19 +43,30 @@ class ObjectEventListView(EventListView):
view_permissions = None view_permissions = None
def dispatch(self, request, *args, **kwargs): 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: 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: except self.content_type.model_class().DoesNotExist:
raise Http404 raise Http404
try: try:
Permission.check_permissions(request.user, permissions=(permission_events_view,)) Permission.check_permissions(
request.user, permissions=(permission_events_view,)
)
except PermissionDenied: 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): def get_queryset(self):
return any_stream(self.content_object) return any_stream(self.content_object)
@@ -75,9 +88,13 @@ class VerbEventListView(SingleObjectListView):
'extra_columns': [ 'extra_columns': [
{ {
'name': _('Target'), 'name': _('Target'),
'attribute': encapsulate(lambda entry: event_object_link(entry)) 'attribute': encapsulate(
lambda entry: event_object_link(entry)
)
} }
], ],
'hide_object': True, '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]} mayan_view_permissions = {'POST': [permission_folder_create]}
def get(self, *args, **kwargs): 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) return super(APIFolderListView, self).get(*args, **kwargs)
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
"""Create a new folder.""" """
Create a new folder.
"""
return super(APIFolderListView, self).post(*args, **kwargs) return super(APIFolderListView, self).post(*args, **kwargs)
def create(self, request, *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(): if serializer.is_valid():
serializer.object.user = request.user serializer.object.user = request.user
@@ -48,8 +54,10 @@ class APIFolderListView(generics.ListCreateAPIView):
self.object = serializer.save(force_insert=True) self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True) self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data) headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, return Response(
headers=headers) serializer.data, status=status.HTTP_201_CREATED,
headers=headers
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -67,24 +75,34 @@ class APIFolderView(generics.RetrieveUpdateDestroyAPIView):
} }
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
"""Delete the selected folder.""" """
Delete the selected folder.
"""
return super(APIFolderView, self).delete(*args, **kwargs) return super(APIFolderView, self).delete(*args, **kwargs)
def get(self, *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) return super(APIFolderView, self).get(*args, **kwargs)
def patch(self, *args, **kwargs): def patch(self, *args, **kwargs):
"""Edit the selected folder.""" """
Edit the selected folder.
"""
return super(APIFolderView, self).patch(*args, **kwargs) return super(APIFolderView, self).patch(*args, **kwargs)
def put(self, *args, **kwargs): def put(self, *args, **kwargs):
"""Edit the selected folder.""" """
Edit the selected folder.
"""
return super(APIFolderView, self).put(*args, **kwargs) return super(APIFolderView, self).put(*args, **kwargs)
class APIFolderDocumentListView(generics.ListAPIView): 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,) filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {'GET': [permission_document_view]} mayan_object_permissions = {'GET': [permission_document_view]}
@@ -96,15 +114,21 @@ class APIFolderDocumentListView(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
folder = get_object_or_404(Folder, pk=self.kwargs['pk']) folder = get_object_or_404(Folder, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(self.request.user, [permission_folder_view]) Permission.check_permissions(
self.request.user, [permission_folder_view]
)
except PermissionDenied: 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() return folder.documents.all()
class APIDocumentFolderListView(generics.ListAPIView): 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 serializer_class = FolderSerializer
@@ -114,9 +138,13 @@ class APIDocumentFolderListView(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
document = get_object_or_404(Document, pk=self.kwargs['pk']) document = get_object_or_404(Document, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(self.request.user, [permission_document_view]) Permission.check_permissions(
self.request.user, [permission_document_view]
)
except PermissionDenied: 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() queryset = document.folders.all()
return queryset return queryset
@@ -125,13 +153,19 @@ class APIDocumentFolderListView(generics.ListAPIView):
class APIFolderDocumentView(views.APIView): class APIFolderDocumentView(views.APIView):
def delete(self, request, *args, **kwargs): 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']) folder = get_object_or_404(Folder, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(request.user, [permission_folder_remove_document]) Permission.check_permissions(
request.user, [permission_folder_remove_document]
)
except PermissionDenied: 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']) document = get_object_or_404(Document, pk=self.kwargs['document_pk'])
folder.documents.remove(document) folder.documents.remove(document)
@@ -139,13 +173,19 @@ class APIFolderDocumentView(views.APIView):
# TODO: move this method as post of APIFolderDocumentListView # TODO: move this method as post of APIFolderDocumentListView
def post(self, request, *args, **kwargs): 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']) folder = get_object_or_404(Folder, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(request.user, [permission_folder_add_document]) Permission.check_permissions(
request.user, [permission_folder_add_document]
)
except PermissionDenied: 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']) document = get_object_or_404(Document, pk=self.kwargs['document_pk'])
folder.documents.add(document) folder.documents.add(document)

View File

@@ -39,24 +39,48 @@ class FoldersApp(MayanAppConfig):
ModelPermission.register( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
permission_folder_add_document, permission_folder_remove_document permission_folder_add_document,
permission_folder_remove_document
) )
) )
ModelPermission.register( ModelPermission.register(
model=Folder, permissions=( model=Folder, permissions=(
permission_acl_edit, permission_acl_view, permission_folder_delete, permission_acl_edit, permission_acl_view,
permission_folder_edit, permission_folder_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_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(
menu_multi_item.bind_links(links=[link_folder_document_multiple_remove], sources=[CombinedSource(obj=Document, view='folders:folder_view')]) links=[link_folder_add_multiple_documents], sources=[Document]
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_multi_item.bind_links(
menu_sidebar.bind_links(links=[link_folder_add_document], sources=['folders:document_folder_list', 'folders:folder_add_document']) 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') SourceColumn(source=Folder, label=_('User'), attribute='user')

View File

@@ -25,7 +25,9 @@ class FolderListForm(forms.Form):
try: try:
Permission.check_permissions(user, [permission_folder_view]) Permission.check_permissions(user, [permission_folder_view])
except PermissionDenied: 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( self.fields['folder'] = forms.ModelChoiceField(
queryset=queryset, queryset=queryset,

View File

@@ -11,12 +11,38 @@ from .permissions import (
permission_folder_remove_document permission_folder_remove_document
) )
link_document_folder_list = Link(permissions=[permission_document_view], text=_('Folders'), view='folders:document_folder_list', args='object.pk') link_document_folder_list = Link(
link_folder_add_document = Link(permissions=[permission_folder_add_document], text=_('Add to a folder'), view='folders:folder_add_document', args='object.pk') permissions=[permission_document_view], text=_('Folders'),
link_folder_add_multiple_documents = Link(text=_('Add to folder'), view='folders:folder_add_multiple_documents') view='folders:document_folder_list', args='object.pk'
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_add_document = Link(
link_folder_document_multiple_remove = Link(permissions=[permission_folder_remove_document], text=_('Remove from folder'), view='folders:folder_document_multiple_remove', args='object.pk') permissions=[permission_folder_add_document], text=_('Add to a folder'),
link_folder_edit = Link(permissions=[permission_folder_edit], text=_('Edit'), view='folders:folder_edit', args='object.pk') view='folders:folder_add_document', 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_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 @python_2_unicode_compatible
class Folder(models.Model): 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')) user = models.ForeignKey(User, verbose_name=_('User'))
datetime_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Datetime created')) datetime_created = models.DateTimeField(
documents = models.ManyToManyField(Document, related_name='folders', verbose_name=_('Documents')) auto_now_add=True, verbose_name=_('Datetime created')
)
documents = models.ManyToManyField(
Document, related_name='folders', verbose_name=_('Documents')
)
def __str__(self): def __str__(self):
return self.label return self.label

View File

@@ -6,9 +6,21 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('folders', _('Folders')) namespace = PermissionNamespace('folders', _('Folders'))
permission_folder_create = namespace.add_permission(name='folder_create', label=_('Create new folders')) permission_folder_create = namespace.add_permission(
permission_folder_edit = namespace.add_permission(name='folder_edit', label=_('Edit new folders')) name='folder_create', label=_('Create 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_edit = namespace.add_permission(
permission_folder_view = namespace.add_permission(name='folder_view', label=_('View existing folders')) name='folder_edit', label=_('Edit new folders')
permission_folder_add_document = namespace.add_permission(name='folder_add_document', label=_('Add documents to existing 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 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): class FolderTestCase(TestCase):
def setUp(self): 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: 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): def tearDown(self):
self.document.delete() self.document.delete()

View File

@@ -16,20 +16,47 @@ urlpatterns = patterns(
url(r'^list/$', FolderListView.as_view(), name='folder_list'), url(r'^list/$', FolderListView.as_view(), name='folder_list'),
url(r'^create/$', FolderCreateView.as_view(), name='folder_create'), url(r'^create/$', FolderCreateView.as_view(), name='folder_create'),
url(r'^(?P<pk>\d+)/edit/$', FolderEditView.as_view(), name='folder_edit'), 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<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(
url(r'^document/multiple/folder/add/$', 'folder_add_multiple_documents', name='folder_add_multiple_documents'), r'^document/(?P<document_id>\d+)/folder/add/$',
url(r'^document/(?P<pk>\d+)/folder/list/$', DocumentFolderListView.as_view(), name='document_folder_list'), '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( api_urls = patterns(
'', '',
url(r'^folders/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$', APIFolderDocumentView.as_view(), name='folder-document'), url(
url(r'^folders/(?P<pk>[0-9]+)/documents/$', APIFolderDocumentListView.as_view(), name='folder-document-list'), r'^folders/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$',
url(r'^folders/(?P<pk>[0-9]+)/$', APIFolderView.as_view(), name='folder-detail'), 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'^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: try:
Permission.check_permissions(user, [permission_document_view]) Permission.check_permissions(user, [permission_document_view])
except PermissionDenied: 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() return queryset.count()
@@ -69,7 +71,14 @@ class FolderListView(SingleObjectListView):
def get_extra_context(self): def get_extra_context(self):
return { return {
'extra_columns': [ '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'), 'title': _('Folders'),
'hide_link': True, 'hide_link': True,
@@ -83,14 +92,21 @@ class FolderCreateView(SingleObjectCreateView):
def form_valid(self, form): def form_valid(self, form):
try: 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: except Folder.DoesNotExist:
instance = form.save(commit=False) instance = form.save(commit=False)
instance.user = self.request.user instance.user = self.request.user
instance.save() instance.save()
return super(FolderCreateView, self).form_valid(form) return super(FolderCreateView, self).form_valid(form)
else: 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) return super(FolderCreateView, self).form_invalid(form)
def get_extra_context(self): def get_extra_context(self):
@@ -105,7 +121,9 @@ def folder_delete(request, folder_id):
try: try:
Permission.check_permissions(request.user, [permission_folder_delete]) Permission.check_permissions(request.user, [permission_folder_delete])
except PermissionDenied: 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') post_action_redirect = reverse('folders:folder_list')
@@ -139,9 +157,13 @@ class FolderDetailView(DocumentListView):
folder = get_object_or_404(Folder, pk=self.kwargs['pk']) folder = get_object_or_404(Folder, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(self.request.user, [permission_folder_view]) Permission.check_permissions(
self.request.user, [permission_folder_view]
)
except PermissionDenied: 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 return folder
@@ -216,9 +238,13 @@ class DocumentFolderListView(FolderListView):
self.document = get_object_or_404(Document, pk=self.kwargs['pk']) self.document = get_object_or_404(Document, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(request.user, [permission_document_view]) Permission.check_permissions(
request.user, [permission_document_view]
)
except PermissionDenied: 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) return super(DocumentFolderListView, self).dispatch(request, *args, **kwargs)
@@ -228,7 +254,14 @@ class DocumentFolderListView(FolderListView):
def get_extra_context(self): def get_extra_context(self):
return { return {
'extra_columns': [ '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, 'hide_link': True,
'object': self.document, 'object': self.document,

View File

@@ -7,7 +7,9 @@ from common.utils import encapsulate
from navigation import SourceColumn from navigation import SourceColumn
from .classes import Property, PropertyNamespace, PIPNotFound, VirtualEnv 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): class InstallationApp(MayanAppConfig):
@@ -17,21 +19,35 @@ class InstallationApp(MayanAppConfig):
def ready(self): def ready(self):
super(InstallationApp, self).ready() super(InstallationApp, self).ready()
SourceColumn(source=PropertyNamespace, label=_('Label'), attribute='label') SourceColumn(
SourceColumn(source=PropertyNamespace, label=_('Items'), attribute=encapsulate(lambda entry: len(entry.get_properties()))) 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=_('Label'), attribute='label')
SourceColumn(source=Property, label=_('Value'), attribute='value') SourceColumn(source=Property, label=_('Value'), attribute='value')
menu_object.bind_links(links=[link_namespace_details], sources=[PropertyNamespace]) menu_object.bind_links(
menu_secondary.bind_links(links=[link_namespace_list], sources=['installation:namespace_list', PropertyNamespace]) 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]) menu_tools.bind_links(links=[link_menu_link])
namespace = PropertyNamespace('venv', _('VirtualEnv')) namespace = PropertyNamespace('venv', _('VirtualEnv'))
try: try:
venv = VirtualEnv() venv = VirtualEnv()
except PIPNotFound: except PIPNotFound:
namespace.add_property('pip', 'pip', _('pip not found.'), report=True) namespace.add_property(
'pip', 'pip', _('pip not found.'), report=True
)
else: else:
for item, version, result in venv.get_results(): 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 # has no version number
return Dependency(string, version=None, standard=True) return Dependency(string, version=None, standard=True)
else: 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) return Dependency(package, version, standard=False)
else: else:
return Dependency(package, version, standard=True) return Dependency(package, version, standard=True)
@@ -146,9 +147,12 @@ class VirtualEnv(object):
if item.version == installed_packages['%s-dev' % name.replace('-', '_')].version: if item.version == installed_packages['%s-dev' % name.replace('-', '_')].version:
status = item.version status = item.version
else: else:
status = installed_packages['%s-dev' % name.replace('-', '_')].version status = installed_packages[
'%s-dev' % name.replace('-', '_')
].version
except KeyError: except KeyError:
# Not installed package found matching with name matchin requirement # Not installed package found matching with name matching
# requirement
status = False status = False
yield name, item.version, status yield name, item.version, status

View File

@@ -6,6 +6,17 @@ from navigation import Link
from .permissions import permission_installation_details 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_menu_link = Link(
link_namespace_details = Link(permissions=[permission_installation_details], text=_('Details'), view='installation:namespace_details', args='object.id') icon='fa fa-check-square-o',
link_namespace_list = Link(permissions=[permission_installation_details], text=_('Installation property namespaces'), view='installation:namespace_list') 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 from permissions import PermissionNamespace
namespace = PermissionNamespace('installation', _('Installation')) 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( urlpatterns = patterns(
'installation.views', 'installation.views',
url(r'^$', 'namespace_list', name='namespace_list'), 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): 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', { return render_to_response('appearance/generic_list.html', {
'object_list': PropertyNamespace.get_all(), 'object_list': PropertyNamespace.get_all(),
@@ -21,7 +23,9 @@ def namespace_list(request):
def namespace_details(request, namespace_id): 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) namespace = PropertyNamespace.get(namespace_id)
object_list = namespace.get_properties() 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_facet.bind_links(
menu_object.bind_links(links=[link_smart_link_condition_edit, link_smart_link_condition_delete], sources=[SmartLinkCondition]) links=[link_smart_link_instances_for_document],
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]) sources=[Document]
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_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_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): class SmartLinkForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SmartLinkForm, self).__init__(*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: class Meta:
fields = ('label', 'dynamic_label', 'enabled') fields = ('label', 'dynamic_label', 'enabled')
@@ -22,8 +29,19 @@ class SmartLinkForm(forms.ModelForm):
class SmartLinkConditionForm(forms.ModelForm): class SmartLinkConditionForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SmartLinkConditionForm, self).__init__(*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['foreign_document_data'] = forms.ChoiceField(
self.fields['expression'].help_text = ' '.join([unicode(self.fields['expression'].help_text), ModelAttribute.help_text_for(Document, type_names=['field', 'related', 'property'])]) 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: class Meta:
model = SmartLinkCondition model = SmartLinkCondition

View File

@@ -10,15 +10,54 @@ from .permissions import (
permission_smart_link_edit, permission_smart_link_view 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_create = Link(
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') permissions=[permission_smart_link_edit], text=_('Create condition'),
link_smart_link_condition_edit = Link(permissions=[permission_smart_link_edit], text=_('Edit'), view='linking:smart_link_condition_edit', args='resolved_object.pk') view='linking:smart_link_condition_create', args='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_condition_delete = Link(
link_smart_link_delete = Link(permissions=[permission_smart_link_delete], tags='dangerous', text=_('Delete'), view='linking:smart_link_delete', args='object.pk') permissions=[permission_smart_link_edit], tags='dangerous',
link_smart_link_document_types = Link(permissions=[permission_smart_link_edit], text=_('Document types'), view='linking:smart_link_document_types', args='object.pk') text=_('Delete'), view='linking:smart_link_condition_delete',
link_smart_link_edit = Link(permissions=[permission_smart_link_edit], text=_('Edit'), view='linking:smart_link_edit', args='object.pk') args='resolved_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_condition_edit = Link(
link_smart_link_list = Link(permissions=[permission_smart_link_create], text=_('Smart links'), view='linking:smart_link_list') permissions=[permission_smart_link_edit], text=_('Edit'),
link_smart_link_setup = Link(icon='fa fa-link', permissions=[permission_smart_link_create], text=_('Smart links'), view='linking:smart_link_list') 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 @python_2_unicode_compatible
class SmartLink(models.Model): class SmartLink(models.Model):
label = models.CharField(max_length=96, verbose_name=_('Label')) 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')) 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): def __str__(self):
return self.label return self.label
@@ -27,19 +34,30 @@ class SmartLink(models.Model):
try: try:
return eval(self.dynamic_label, {'document': document}) return eval(self.dynamic_label, {'document': document})
except Exception as exception: except Exception as exception:
return Exception(_('Error generating dynamic label; %s' % unicode(exception))) return Exception(
_(
'Error generating dynamic label; %s' % unicode(exception)
)
)
else: else:
return self.label return self.label
def get_linked_document_for(self, document): def get_linked_document_for(self, document):
if document.document_type.pk not in self.document_types.values_list('pk', flat=True): 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() smart_link_query = Q()
for condition in self.conditions.filter(enabled=True): for condition in self.conditions.filter(enabled=True):
condition_query = Q(**{ 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: if condition.negated:
condition_query = ~condition_query condition_query = ~condition_query
@@ -55,7 +73,9 @@ class SmartLink(models.Model):
return Document.objects.none() return Document.objects.none()
def resolve_for(self, document): 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: class Meta:
verbose_name = _('Smart link') verbose_name = _('Smart link')
@@ -69,16 +89,36 @@ class ResolvedSmartLink(SmartLink):
@python_2_unicode_compatible @python_2_unicode_compatible
class SmartLinkCondition(models.Model): class SmartLinkCondition(models.Model):
smart_link = models.ForeignKey(SmartLink, related_name='conditions', verbose_name=_('Smart link')) smart_link = models.ForeignKey(
inclusion = models.CharField(choices=INCLUSION_CHOICES, default=INCLUSION_AND, help_text=_('The inclusion is ignored for the first item.'), max_length=16) SmartLink, related_name='conditions', verbose_name=_('Smart link')
foreign_document_data = models.CharField(help_text=_('This represents the metadata of all other documents.'), max_length=128, verbose_name=_('Foreign document attribute')) )
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) 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')) expression = models.TextField(
negated = models.BooleanField(default=False, help_text=_('Inverts the logic of the operator.'), verbose_name=_('Negated')) 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')) enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
def __str__(self): 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: class Meta:
verbose_name = _('Link condition') verbose_name = _('Link condition')

View File

@@ -6,7 +6,15 @@ from permissions import PermissionNamespace
namespace = PermissionNamespace('linking', _('Smart links')) namespace = PermissionNamespace('linking', _('Smart links'))
permission_smart_link_view = namespace.add_permission(name='smart_link_view', label=_('View existing smart links')) permission_smart_link_view = namespace.add_permission(
permission_smart_link_create = namespace.add_permission(name='smart_link_create', label=_('Create new smart links')) name='smart_link_view', label=_('View existing 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_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( urlpatterns = patterns(
'linking.views', 'linking.views',
url(r'^document/(?P<pk>\d+)/list/$', DocumentSmartLinkListView.as_view(), name='smart_link_instances_for_document'), url(
url(r'^document/(?P<document_pk>\d+)/(?P<smart_link_pk>\d+)/$', ResolvedSmartLinkView.as_view(), name='smart_link_instance_view'), 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/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(
url(r'^setup/(?P<smart_link_pk>\d+)/edit/$', 'smart_link_edit', name='smart_link_edit'), r'^setup/(?P<smart_link_pk>\d+)/delete/$', 'smart_link_delete',
url(r'^setup/(?P<pk>\d+)/document_types/$', SetupSmartLinkDocumentTypesView.as_view(), name='smart_link_document_types'), 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(
url(r'^setup/(?P<smart_link_pk>\d+)/condition/create/$', 'smart_link_condition_create', name='smart_link_condition_create'), r'^setup/(?P<smart_link_pk>\d+)/condition/list/$',
url(r'^setup/smart_link/condition/(?P<smart_link_condition_pk>\d+)/edit/$', 'smart_link_condition_edit', name='smart_link_condition_edit'), 'smart_link_condition_list', name='smart_link_condition_list'
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/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']) return get_object_or_404(SmartLink, pk=self.kwargs['pk'])
def left_list(self): 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): 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): def remove(self, item):
self.get_object().document_types.remove(item) self.get_object().document_types.remove(item)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super(SetupSmartLinkDocumentTypesView, self).get_context_data(**kwargs) data = super(
SetupSmartLinkDocumentTypesView, self
).get_context_data(**kwargs)
data.update({ data.update({
'object': self.get_object(), '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 return data
@@ -63,20 +73,34 @@ class SetupSmartLinkDocumentTypesView(AssignRemoveView):
class ResolvedSmartLinkView(DocumentListView): class ResolvedSmartLinkView(DocumentListView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(Document, pk=self.kwargs['document_pk']) self.document = get_object_or_404(
self.smart_link = get_object_or_404(SmartLink, pk=self.kwargs['smart_link_pk']) Document, pk=self.kwargs['document_pk']
)
self.smart_link = get_object_or_404(
SmartLink, pk=self.kwargs['smart_link_pk']
)
try: try:
Permission.check_permissions(request.user, [permission_document_view]) Permission.check_permissions(
request.user, [permission_document_view]
)
except PermissionDenied: 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: try:
Permission.check_permissions(request.user, [permission_smart_link_view]) Permission.check_permissions(
request.user, [permission_smart_link_view]
)
except PermissionDenied: 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): def get_document_queryset(self):
try: try:
@@ -85,7 +109,9 @@ class ResolvedSmartLinkView(DocumentListView):
queryset = Document.objects.none() queryset = Document.objects.none()
if self.request.user.is_staff or self.request.user.is_superuser: 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 return queryset
@@ -93,7 +119,10 @@ class ResolvedSmartLinkView(DocumentListView):
return { return {
'hide_links': True, 'hide_links': True,
'object': self.document, '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, 'document': self.document,
'smart_link': self.smart_link.get_dynamic_label(self.document), 'smart_link': self.smart_link.get_dynamic_label(self.document),
} }
@@ -114,7 +143,11 @@ class SmartLinkListView(SingleObjectListView):
return { return {
'extra_columns': [ 'extra_columns': [
{'name': _('Dynamic label'), 'attribute': 'dynamic_label'}, {'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, 'hide_link': True,
'title': _('Smart links'), 'title': _('Smart links'),
@@ -126,20 +159,34 @@ class DocumentSmartLinkListView(SmartLinkListView):
self.document = get_object_or_404(Document, pk=self.kwargs['pk']) self.document = get_object_or_404(Document, pk=self.kwargs['pk'])
try: try:
Permission.check_permissions(request.user, (permission_document_view,)) Permission.check_permissions(
request.user, (permission_document_view,)
)
except PermissionDenied: 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): 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): def get_extra_context(self):
return { return {
'document': self.document, 'document': self.document,
'extra_columns': ( '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_object': True,
'hide_link': True, 'hide_link': True,
@@ -149,13 +196,19 @@ class DocumentSmartLinkListView(SmartLinkListView):
def smart_link_create(request): 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': if request.method == 'POST':
form = SmartLinkForm(request.POST) form = SmartLinkForm(request.POST)
if form.is_valid(): if form.is_valid():
document_group = form.save() 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')) return HttpResponseRedirect(reverse('linking:smart_link_list'))
else: else:
form = SmartLinkForm() 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) smart_link = get_object_or_404(SmartLink, pk=smart_link_pk)
try: try:
Permission.check_permissions(request.user, [permission_smart_link_edit]) Permission.check_permissions(
request.user, [permission_smart_link_edit]
)
except PermissionDenied: 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': if request.method == 'POST':
form = SmartLinkForm(request.POST, instance=smart_link) form = SmartLinkForm(request.POST, instance=smart_link)
if form.is_valid(): if form.is_valid():
smart_link = form.save() 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')) return HttpResponseRedirect(reverse('linking:smart_link_list'))
else: else:
form = SmartLinkForm(instance=smart_link) 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) smart_link = get_object_or_404(SmartLink, pk=smart_link_pk)
try: try:
Permission.check_permissions(request.user, [permission_smart_link_delete]) Permission.check_permissions(
request.user, [permission_smart_link_delete]
)
except PermissionDenied: 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)))) next = request.POST.get(
previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL)))) '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': if request.method == 'POST':
try: try:
smart_link.delete() 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: except Exception as exception:
messages.error(request, _('Error deleting smart link: %(smart_link)s; %(exception)s.') % { messages.error(
'smart_link': smart_link, request, _(
'exception': exception 'Error deleting smart link: %(smart_link)s; '
}) '%(exception)s.'
) % {
'smart_link': smart_link,
'exception': exception
}
)
return HttpResponseRedirect(next) return HttpResponseRedirect(next)
return render_to_response('appearance/generic_confirm.html', { 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) smart_link = get_object_or_404(SmartLink, pk=smart_link_pk)
try: try:
Permission.check_permissions(request.user, [permission_smart_link_edit]) Permission.check_permissions(
request.user, [permission_smart_link_edit]
)
except PermissionDenied: 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', { return render_to_response('appearance/generic_list.html', {
'title': _('Conditions for smart link: %s') % smart_link, 'title': _('Conditions for smart link: %s') % smart_link,
'object_list': smart_link.conditions.all(), 'object_list': smart_link.conditions.all(),
'extra_columns': [ '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, 'hide_link': True,
'object': smart_link, '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) smart_link = get_object_or_404(SmartLink, pk=smart_link_pk)
try: try:
Permission.check_permissions(request.user, [permission_smart_link_edit]) Permission.check_permissions(
request.user, [permission_smart_link_edit]
)
except PermissionDenied: 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': if request.method == 'POST':
form = SmartLinkConditionForm(data=request.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 = form.save(commit=False)
new_smart_link_condition.smart_link = smart_link new_smart_link_condition.smart_link = smart_link
new_smart_link_condition.save() new_smart_link_condition.save()
messages.success(request, _('Smart link condition: "%s" created successfully.') % new_smart_link_condition) messages.success(
return HttpResponseRedirect(reverse('linking:smart_link_condition_list', args=[smart_link.pk])) request, _(
'Smart link condition: "%s" created successfully.'
) % new_smart_link_condition
)
return HttpResponseRedirect(
reverse(
'linking:smart_link_condition_list', args=[smart_link.pk]
)
)
else: else:
form = SmartLinkConditionForm() 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): 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: try:
Permission.check_permissions(request.user, [permission_smart_link_edit]) Permission.check_permissions(
request.user, [permission_smart_link_edit]
)
except PermissionDenied: 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)))) 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)))) previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
if request.method == 'POST': if request.method == 'POST':
form = SmartLinkConditionForm(request.POST, instance=smart_link_condition) form = SmartLinkConditionForm(
request.POST, instance=smart_link_condition
)
if form.is_valid(): if form.is_valid():
smart_link_condition = form.save() 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) return HttpResponseRedirect(next)
else: else:
form = SmartLinkConditionForm(instance=smart_link_condition) 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): 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: try:
Permission.check_permissions(request.user, [permission_smart_link_edit]) Permission.check_permissions(request.user, [permission_smart_link_edit])
except PermissionDenied: 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)))) 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)))) 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': if request.method == 'POST':
try: try:
smart_link_condition.delete() 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: except Exception as exception:
messages.error(request, _('Error deleting smart link condition: %(smart_link_condition)s; %(exception)s.') % { messages.error(
'smart_link_condition': smart_link_condition, request, _(
'exception': exception 'Error deleting smart link condition: '
}) '%(smart_link_condition)s; %(exception)s.'
) % {
'smart_link_condition': smart_link_condition,
'exception': exception
}
)
return HttpResponseRedirect(next) return HttpResponseRedirect(next)
return render_to_response('appearance/generic_confirm.html', { 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) logger.debug('unable to acquire lock: %s', name)
raise LockError('Unable to acquire lock') raise LockError('Unable to acquire lock')
except OperationalError as exception: 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: else:
logger.debug('acquired lock: %s', name) logger.debug('acquired lock: %s', name)
return lock return lock

View File

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

View File

@@ -4,4 +4,6 @@ from django.conf import settings
DEFAULT_LOCK_TIMEOUT_VALUE = 30 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( migrations.CreateModel(
name='DocumentMetadata', name='DocumentMetadata',
fields=[ 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)), 'id', models.AutoField(
('document', models.ForeignKey(related_name='metadata', verbose_name='Document', to='documents.Document')), 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={ options={
'verbose_name': 'Document metadata', 'verbose_name': 'Document metadata',
@@ -27,9 +42,23 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='DocumentTypeMetadataType', name='DocumentTypeMetadataType',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
('required', models.BooleanField(default=False, verbose_name='Required')), 'id', models.AutoField(
('document_type', models.ForeignKey(related_name='metadata', verbose_name='Document type', to='documents.DocumentType')), 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={ options={
'verbose_name': 'Document type metadata type options', 'verbose_name': 'Document type metadata type options',
@@ -40,12 +69,47 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='MetadataType', name='MetadataType',
fields=[ 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')), 'id', models.AutoField(
('title', models.CharField(max_length=48, verbose_name='Title')), verbose_name='ID', serialize=False,
('default', models.CharField(help_text='Enter a string to be evaluated.', max_length=128, null=True, verbose_name='Default', blank=True)), auto_created=True, primary_key=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')])), ),
(
'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={ options={
'ordering': ('title',), 'ordering': ('title',),
@@ -57,7 +121,9 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='documenttypemetadatatype', model_name='documenttypemetadatatype',
name='metadata_type', 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, preserve_default=True,
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
@@ -67,7 +133,9 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='documentmetadata', model_name='documentmetadata',
name='metadata_type', name='metadata_type',
field=models.ForeignKey(verbose_name='Type', to='metadata.MetadataType'), field=models.ForeignKey(
verbose_name='Type', to='metadata.MetadataType'
),
preserve_default=True, preserve_default=True,
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(

View File

@@ -13,7 +13,10 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='metadatatype', 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( migrations.RenameField(
model_name='metadatatype', model_name='metadatatype',

View File

@@ -11,8 +11,8 @@ from permissions import Permission
class MayanObjectPermissionsFilter(BaseFilterBackend): class MayanObjectPermissionsFilter(BaseFilterBackend):
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
required_permission = getattr( required_permission = getattr(
view, 'mayan_object_permissions', {}).get(request.method, None view, 'mayan_object_permissions', {}
) ).get(request.method, None)
if required_permission: if required_permission:
try: try:

View File

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

View File

@@ -8,7 +8,8 @@ version_0_urlpatterns = patterns(
'', '',
url(r'^$', Version_0.as_view(), name='api-version-0'), url(r'^$', Version_0.as_view(), name='api-version-0'),
url( 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), 'name': unicode(endpoint),
'url': reverse('api-version-0-app', '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() } for endpoint in APIEndPoint.get_all()
], ],
}) })

View File

@@ -40,7 +40,9 @@ class SmartSettingsApp(MayanAppConfig):
) )
SourceColumn( SourceColumn(
source=Setting, label=_('Found in path'), 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( menu_object.bind_links(
@@ -54,4 +56,6 @@ class SmartSettingsApp(MayanAppConfig):
except ImportError: except ImportError:
logger.debug('App %s has not settings.py file', app.name) logger.debug('App %s has not settings.py file', app.name)
else: 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( urlpatterns = patterns(
'', '',
url( url(
r'^namespace/all/$', NamespaceListView.as_view(), name='namespace_list' r'^namespace/all/$', NamespaceListView.as_view(),
name='namespace_list'
), ),
url( url(
r'^namespace/(?P<namespace_name>\w+)/$', NamespaceDetailView.as_view(), r'^namespace/(?P<namespace_name>\w+)/$',
name='namespace_detail' NamespaceDetailView.as_view(), name='namespace_detail'
), ),
) )

View File

@@ -49,7 +49,11 @@ class SourcesApp(MayanAppConfig):
MissingItem( MissingItem(
label=_('Create a document source'), 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(), condition=lambda: not Source.objects.exists(),
view='sources:setup_source_list' view='sources:setup_source_list'
) )
@@ -131,7 +135,8 @@ class SourcesApp(MayanAppConfig):
) )
post_upgrade.connect( post_upgrade.connect(
initialize_periodic_tasks, dispatch_uid='initialize_periodic_tasks' initialize_periodic_tasks,
dispatch_uid='initialize_periodic_tasks'
) )
post_initial_setup.connect( post_initial_setup.connect(
create_default_document_source, create_default_document_source,

View File

@@ -51,7 +51,7 @@ class UploadDocumentTestCase(TestCase):
self.client.post( self.client.post(
reverse( reverse(
'sources:setup_source_create', args=[SOURCE_CHOICE_WEB_FORM] 'sources:setup_source_create', args=[SOURCE_CHOICE_WEB_FORM]
), {'label': 'test', 'uncompress': 'n', 'enabled': True} ), {'label': 'test', 'uncompress': 'n', 'enabled': True}
) )
self.assertEqual(WebFormSource.objects.count(), 1) self.assertEqual(WebFormSource.objects.count(), 1)

Some files were not shown because too many files have changed in this diff Show More