diff --git a/MANIFEST.in b/MANIFEST.in
index 9092355f53..7a3241b6e8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,3 @@
include README.rst LICENSE HISTORY.rst
recursive-include mayan README *.txt *.html *.css *.ico *.png *.jpg *.js
-global-exclude settings_local.* mayan.sqlite* db.sqlite* gpg_home document_storage image_cache
+global-exclude settings_local.* mayan.sqlite* db.sqlite* mayan/media gpg_home document_storage image_cache
diff --git a/README.rst b/README.rst
index 80284ff95f..6a3c5f54b9 100644
--- a/README.rst
+++ b/README.rst
@@ -34,7 +34,7 @@ To install **Mayan EDMS**, simply do:
$ virtualenv venv
$ source venv/bin/activate
- $ pip install mayan-edms==1.0.rc2
+ $ pip install mayan-edms==1.0.rc3
$ mayan-edms.py initialsetup
$ mayan-edms.py runserver
diff --git a/docs/credits/contributors.rst b/docs/credits/contributors.rst
index e399d26b95..2871425553 100644
--- a/docs/credits/contributors.rst
+++ b/docs/credits/contributors.rst
@@ -20,6 +20,7 @@ Contributors (in alphabetical order)
* Bertrand Bordage (https://github.com/BertrandBordage)
* Brian E (brian@realize.org)
* David Herring (https://github.com/abadger1406)
+* Jens Kadenbach (https://github.com/audax)
* Kolmar Kafran
* IHLeanne (https://github.com/IHLeanne)
* Iliya Georgiev (ikgeorgiev@gmail.com)
diff --git a/docs/intro/installation.rst b/docs/intro/installation.rst
index 3dd79dc550..75b00bfbb9 100644
--- a/docs/intro/installation.rst
+++ b/docs/intro/installation.rst
@@ -21,7 +21,7 @@ Initialize a ``virtualenv`` to deploy the project:
$ virtualenv venv
$ source venv/bin/activate
- $ pip install mayan-edms==1.0.rc2
+ $ pip install mayan-edms==1.0.rc3
By default **Mayan EDMS** will create a single file SQLite_ database, which makes
it very easy to start using **Mayan EDMS**. Populate the database with the project's schema doing:
diff --git a/mayan/__init__.py b/mayan/__init__.py
index a8612c344d..fbb2a7fb86 100644
--- a/mayan/__init__.py
+++ b/mayan/__init__.py
@@ -1,5 +1,5 @@
__title__ = 'Mayan EDMS'
-__version__ = '1.0 rc2'
+__version__ = '1.0 rc3'
__build__ = 0x010000
__author__ = 'Roberto Rosario'
__license__ = 'Apache 2.0'
diff --git a/mayan/apps/bootstrap/views.py b/mayan/apps/bootstrap/views.py
index 40a4b737e0..694d067b2b 100644
--- a/mayan/apps/bootstrap/views.py
+++ b/mayan/apps/bootstrap/views.py
@@ -163,8 +163,8 @@ def bootstrap_setup_execute(request, bootstrap_setup_pk):
bootstrap_setup.execute()
except ExistingData:
messages.error(request, _(u'Cannot execute bootstrap setup, there is existing data. Erase all data and try again.'))
- except Exception, exc:
- messages.error(request, _(u'Error executing bootstrap setup; %s') % exc)
+ except Exception as exception:
+ messages.error(request, _(u'Error executing bootstrap setup; %s') % exception)
else:
messages.success(request, _(u'Bootstrap setup "%s" executed successfully.') % bootstrap_setup)
return HttpResponseRedirect(next)
@@ -295,8 +295,8 @@ def erase_database_view(request):
if request.method == 'POST':
try:
Cleanup.execute_all()
- except Exception, exc:
- messages.error(request, _(u'Error erasing database; %s') % exc)
+ except Exception as exception:
+ messages.error(request, _(u'Error erasing database; %s') % exception)
else:
messages.success(request, _(u'Database erased successfully.'))
return HttpResponseRedirect(next)
diff --git a/mayan/apps/checkouts/views.py b/mayan/apps/checkouts/views.py
index 8102d24d03..1e611e122c 100644
--- a/mayan/apps/checkouts/views.py
+++ b/mayan/apps/checkouts/views.py
@@ -78,8 +78,8 @@ def checkout_document(request, document_pk):
document_checkout = form.save(commit=False)
document_checkout.user_object = request.user
document_checkout.save()
- except Exception, exc:
- messages.error(request, _(u'Error trying to check out document; %s') % exc)
+ except Exception as exception:
+ messages.error(request, _(u'Error trying to check out document; %s') % exception)
else:
messages.success(request, _(u'Document "%s" checked out successfully.') % document)
return HttpResponseRedirect(reverse('checkout_info', args=[document.pk]))
@@ -122,8 +122,8 @@ def checkin_document(request, document_pk):
document.check_in(user=request.user)
except DocumentNotCheckedOut:
messages.error(request, _(u'Document has not been checked out.'))
- except Exception, exc:
- messages.error(request, _(u'Error trying to check in document; %s') % exc)
+ except Exception as exception:
+ messages.error(request, _(u'Error trying to check in document; %s') % exception)
else:
messages.success(request, _(u'Document "%s" checked in successfully.') % document)
return HttpResponseRedirect(next)
diff --git a/mayan/apps/common/middleware/strip_spaces_widdleware.py b/mayan/apps/common/middleware/strip_spaces_widdleware.py
index eb75e7f597..ddfa4edf52 100644
--- a/mayan/apps/common/middleware/strip_spaces_widdleware.py
+++ b/mayan/apps/common/middleware/strip_spaces_widdleware.py
@@ -1,9 +1,12 @@
-# Aliasing it for the sake of page size.
-from django.utils.html import strip_spaces_between_tags as short
+from django.utils.html import strip_spaces_between_tags
class SpacelessMiddleware(object):
+ """
+ Remove spaces between tags in HTML responses to save on bandwidth
+ """
+
def process_response(self, request, response):
- if u'text/html' in response['Content-Type']:
- response.content = short(response.content)
+ if 'text/html' in response.get('Content-Type', ''):
+ response.content = strip_spaces_between_tags(response.content)
return response
diff --git a/mayan/apps/common/utils.py b/mayan/apps/common/utils.py
index 0c8d6ec103..20d921ce9f 100644
--- a/mayan/apps/common/utils.py
+++ b/mayan/apps/common/utils.py
@@ -78,9 +78,9 @@ def return_attrib(obj, attrib, arguments=None):
return result()
else:
return result
- except Exception, err:
+ except Exception as exception:
if settings.DEBUG:
- return 'Attribute error: %s; %s' % (attrib, err)
+ return 'Attribute error: %s; %s' % (attrib, exception)
else:
pass
diff --git a/mayan/apps/common/widgets.py b/mayan/apps/common/widgets.py
index a134f788d0..cafcb3e890 100644
--- a/mayan/apps/common/widgets.py
+++ b/mayan/apps/common/widgets.py
@@ -62,8 +62,8 @@ class DetailSelectMultiple(forms.widgets.SelectMultiple):
def exists_with_famfam(path):
try:
return two_state_template(os.path.exists(path))
- except Exception, exc:
- return exc
+ except Exception as exception:
+ return exception
def two_state_template(state, famfam_ok_icon=u'tick', famfam_fail_icon=u'cross'):
diff --git a/mayan/apps/converter/office_converter.py b/mayan/apps/converter/office_converter.py
index fd31d13eb6..d94ae91bec 100644
--- a/mayan/apps/converter/office_converter.py
+++ b/mayan/apps/converter/office_converter.py
@@ -85,9 +85,9 @@ class OfficeConverter(object):
try:
self.backend.convert(self.input_filepath, self.output_filepath)
self.exists = True
- except OfficeBackendError, msg:
+ except OfficeBackendError as exception:
# convert exception so that at least the mime type icon is displayed
- raise UnknownFileFormat(msg)
+ raise UnknownFileFormat(exception)
def __unicode__(self):
return getattr(self, 'output_filepath', None)
@@ -140,7 +140,7 @@ class OfficeConverterBackendDirect(object):
logger.debug('converted_output: %s' % converted_output)
os.rename(converted_output, self.output_filepath)
- except OSError, msg:
- raise OfficeBackendError(msg)
- except Exception, msg:
- logger.error('Unhandled exception', exc_info=msg)
+ except OSError as exception:
+ raise OfficeBackendError(exception)
+ except Exception as exception:
+ logger.error('Unhandled exception', exc_info=exception)
diff --git a/mayan/apps/django_gpg/views.py b/mayan/apps/django_gpg/views.py
index 21e2720e1d..441775b749 100644
--- a/mayan/apps/django_gpg/views.py
+++ b/mayan/apps/django_gpg/views.py
@@ -100,8 +100,8 @@ def key_delete(request, fingerprint, key_type):
gpg.delete_key(key)
messages.success(request, _(u'Key: %s, deleted successfully.') % fingerprint)
return HttpResponseRedirect(next)
- except Exception, msg:
- messages.error(request, msg)
+ except Exception as exception:
+ messages.error(request, exception)
return HttpResponseRedirect(previous)
return render_to_response('generic_confirm.html', {
diff --git a/mayan/apps/document_indexing/api.py b/mayan/apps/document_indexing/api.py
index a059512543..7504ff6056 100644
--- a/mayan/apps/document_indexing/api.py
+++ b/mayan/apps/document_indexing/api.py
@@ -81,18 +81,18 @@ def cascade_eval(eval_dict, document, template_node, parent_index_instance=None)
if template_node.enabled:
try:
result = eval(template_node.expression, eval_dict, AVAILABLE_INDEXING_FUNCTIONS)
- except Exception, exc:
+ except Exception as exception:
warnings.append(_(u'Error in document indexing update expression: %(expression)s; %(exception)s') % {
- 'expression': template_node.expression, 'exception': exc})
+ 'expression': template_node.expression, 'exception': exception})
else:
if result:
index_instance, created = IndexInstanceNode.objects.get_or_create(index_template_node=template_node, value=result, parent=parent_index_instance)
# if created:
try:
fs_create_index_directory(index_instance)
- except Exception, exc:
+ except Exception as exception:
warnings.append(_(u'Error updating document index, expression: %(expression)s; %(exception)s') % {
- 'expression': template_node.expression, 'exception': exc})
+ 'expression': template_node.expression, 'exception': exception})
if template_node.link_documents:
suffix = find_lowest_available_suffix(index_instance, document)
@@ -105,9 +105,9 @@ def cascade_eval(eval_dict, document, template_node, parent_index_instance=None)
try:
fs_create_document_link(index_instance, document, suffix)
- except Exception, exc:
+ except Exception as exception:
warnings.append(_(u'Error updating document index, expression: %(expression)s; %(exception)s') % {
- 'expression': template_node.expression, 'exception': exc})
+ 'expression': template_node.expression, 'exception': exception})
index_instance.documents.add(document)
@@ -147,7 +147,7 @@ def cascade_document_remove(document, index_instance):
warnings.extend(parent_warnings)
except DocumentRenameCount.DoesNotExist:
return warnings
- except Exception, exc:
- warnings.append(_(u'Unable to delete document indexing node; %s') % exc)
+ except Exception as exception:
+ warnings.append(_(u'Unable to delete document indexing node; %s') % exception)
return warnings
diff --git a/mayan/apps/document_indexing/filesystem.py b/mayan/apps/document_indexing/filesystem.py
index bf6ead9655..0158b92cdf 100644
--- a/mayan/apps/document_indexing/filesystem.py
+++ b/mayan/apps/document_indexing/filesystem.py
@@ -44,11 +44,11 @@ def fs_create_index_directory(index_instance):
target_directory = assemble_path_from_list([FILESYSTEM_SERVING[index_instance.index_template_node.index.name], get_instance_path(index_instance)])
try:
os.mkdir(target_directory)
- except OSError, exc:
- if exc.errno == errno.EEXIST:
+ except OSError as exception:
+ if exception.errno == errno.EEXIST:
pass
else:
- raise Exception(_(u'Unable to create indexing directory; %s') % exc)
+ raise Exception(_(u'Unable to create indexing directory; %s') % exception)
def fs_create_document_link(index_instance, document, suffix=0):
@@ -58,17 +58,17 @@ def fs_create_document_link(index_instance, document, suffix=0):
try:
os.symlink(document.file.path, filepath)
- except OSError, exc:
- if exc.errno == errno.EEXIST:
+ except OSError as exception:
+ if exception.errno == errno.EEXIST:
# This link should not exist, try to delete it
try:
os.unlink(filepath)
# Try again
os.symlink(document.file.path, filepath)
- except Exception, exc:
- raise Exception(_(u'Unable to create symbolic link, file exists and could not be deleted: %(filepath)s; %(exc)s') % {'filepath': filepath, 'exc': exc})
+ except Exception as exception:
+ raise Exception(_(u'Unable to create symbolic link, file exists and could not be deleted: %(filepath)s; %(exception)s') % {'filepath': filepath, 'exception': exception})
else:
- raise Exception(_(u'Unable to create symbolic link: %(filepath)s; %(exc)s') % {'filepath': filepath, 'exc': exc})
+ raise Exception(_(u'Unable to create symbolic link: %(filepath)s; %(exception)s') % {'filepath': filepath, 'exception': exception})
def fs_delete_document_link(index_instance, document, suffix=0):
@@ -78,10 +78,10 @@ def fs_delete_document_link(index_instance, document, suffix=0):
try:
os.unlink(filepath)
- except OSError, exc:
- if exc.errno != errno.ENOENT:
+ except OSError as exception:
+ if exception.errno != errno.ENOENT:
# Raise when any error other than doesn't exits
- raise Exception(_(u'Unable to delete document symbolic link; %s') % exc)
+ raise Exception(_(u'Unable to delete document symbolic link; %s') % exception)
def fs_delete_index_directory(index_instance):
@@ -89,11 +89,11 @@ def fs_delete_index_directory(index_instance):
target_directory = assemble_path_from_list([FILESYSTEM_SERVING[index_instance.index_template_node.index.name], get_instance_path(index_instance)])
try:
os.removedirs(target_directory)
- except OSError, exc:
- if exc.errno == errno.EEXIST:
+ except OSError as exception:
+ if exception.errno == errno.EEXIST:
pass
else:
- raise Exception(_(u'Unable to delete indexing directory; %s') % exc)
+ raise Exception(_(u'Unable to delete indexing directory; %s') % exception)
def fs_delete_directory_recusive(index):
diff --git a/mayan/apps/document_signatures/managers.py b/mayan/apps/document_signatures/managers.py
index 6f0fbc888a..4898df1828 100644
--- a/mayan/apps/document_signatures/managers.py
+++ b/mayan/apps/document_signatures/managers.py
@@ -32,19 +32,25 @@ class DocumentVersionSignatureManager(models.Manager):
document_signature.save()
def has_detached_signature(self, document):
- document_signature = self.get_document_signature(document)
-
- if document_signature.signature_file:
- return True
- else:
+ try:
+ document_signature = self.get_document_signature(document)
+ except ValueError:
return False
+ else:
+ if document_signature.signature_file:
+ return True
+ else:
+ return False
def has_embedded_signature(self, document):
logger.debug('document: %s' % document)
- document_signature = self.get_document_signature(document)
-
- return document_signature.has_embedded_signature
+ try:
+ document_signature = self.get_document_signature(document)
+ except ValueError:
+ return False
+ else:
+ return document_signature.has_embedded_signature
def detached_signature(self, document):
document_signature = self.get_document_signature(document)
diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py
index 19cfe26e8b..14d1a550aa 100644
--- a/mayan/apps/document_signatures/views.py
+++ b/mayan/apps/document_signatures/views.py
@@ -15,7 +15,7 @@ from django.utils.translation import ugettext_lazy as _
from acls.models import AccessEntry
from filetransfers.api import serve_file
-from django_gpg.api import SIGNATURE_STATES
+from django_gpg.api import SIGNATURE_STATE_NONE, SIGNATURE_STATES
from documents.models import Document, RecentDocument
from permissions.models import Permission
@@ -37,9 +37,13 @@ def document_verify(request, document_pk):
RecentDocument.objects.add_document_for_user(request.user, document)
- signature = DocumentVersionSignature.objects.verify_signature(document)
-
- signature_state = SIGNATURE_STATES.get(getattr(signature, 'status', None))
+ try:
+ signature = DocumentVersionSignature.objects.verify_signature(document)
+ except AttributeError:
+ signature_state = SIGNATURE_STATES.get(SIGNATURE_STATE_NONE)
+ signature = None
+ else:
+ signature_state = SIGNATURE_STATES.get(getattr(signature, 'status', None))
widget = (u'
' % (settings.STATIC_URL, signature_state['icon']))
paragraphs = [
@@ -49,10 +53,13 @@ def document_verify(request, document_pk):
},
]
- if DocumentVersionSignature.objects.has_embedded_signature(document):
- signature_type = _(u'embedded')
- else:
- signature_type = _(u'detached')
+ try:
+ if DocumentVersionSignature.objects.has_embedded_signature(document):
+ signature_type = _(u'embedded')
+ else:
+ signature_type = _(u'detached')
+ except ValueError:
+ signature_type = _(u'None')
if signature:
paragraphs.extend(
@@ -94,8 +101,8 @@ def document_signature_upload(request, document_pk):
DocumentVersionSignature.objects.add_detached_signature(document, request.FILES['file'])
messages.success(request, _(u'Detached signature uploaded successfully.'))
return HttpResponseRedirect(next)
- except Exception, msg:
- messages.error(request, msg)
+ except Exception as exception:
+ messages.error(request, exception)
return HttpResponseRedirect(previous)
else:
form = DetachedSignatureForm()
@@ -153,8 +160,8 @@ def document_signature_delete(request, document_pk):
DocumentVersionSignature.objects.clear_detached_signature(document)
messages.success(request, _(u'Detached signature deleted successfully.'))
return HttpResponseRedirect(next)
- except Exception, exc:
- messages.error(request, _(u'Error while deleting the detached signature; %s') % exc)
+ except Exception as exception:
+ messages.error(request, _(u'Error while deleting the detached signature; %s') % exception)
return HttpResponseRedirect(previous)
return render_to_response('generic_confirm.html', {
diff --git a/mayan/apps/documents/__init__.py b/mayan/apps/documents/__init__.py
index d67ffef569..6e93f6a158 100644
--- a/mayan/apps/documents/__init__.py
+++ b/mayan/apps/documents/__init__.py
@@ -100,7 +100,7 @@ register_maintenance_links([document_find_all_duplicates, document_update_page_c
register_model_list_columns(Document, [
{
'name': _(u'thumbnail'), 'attribute':
- encapsulate(lambda x: document_thumbnail(x, gallery_name='document_list', title=x.filename, size=THUMBNAIL_SIZE))
+ encapsulate(lambda x: document_thumbnail(x, gallery_name='document_list', title=getattr(x, 'filename', None), size=THUMBNAIL_SIZE))
},
])
diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py
index 5d1de99c2b..0ecaccd767 100644
--- a/mayan/apps/documents/api_views.py
+++ b/mayan/apps/documents/api_views.py
@@ -3,25 +3,29 @@ from __future__ import absolute_import
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
+from rest_framework import generics, status
+from rest_framework.response import Response
+
+from acls.models import AccessEntry
from converter.exceptions import UnkownConvertError, UnknownFileFormat
from converter.literals import (DEFAULT_PAGE_NUMBER, DEFAULT_ROTATION,
DEFAULT_ZOOM_LEVEL)
from permissions.models import Permission
-from rest_framework import generics
-from rest_framework.response import Response
-
-from acls.models import AccessEntry
from rest_api.filters import MayanObjectPermissionsFilter
from rest_api.permissions import MayanPermission
from .conf.settings import DISPLAY_SIZE, ZOOM_MAX_LEVEL, ZOOM_MIN_LEVEL
-from .permissions import PERMISSION_DOCUMENT_VIEW
from .models import Document, DocumentPage, DocumentVersion
+from .permissions import (PERMISSION_DOCUMENT_CREATE,
+ PERMISSION_DOCUMENT_DELETE, PERMISSION_DOCUMENT_EDIT,
+ PERMISSION_DOCUMENT_NEW_VERSION,
+ PERMISSION_DOCUMENT_PROPERTIES_EDIT,
+ PERMISSION_DOCUMENT_VIEW)
from .serializers import (DocumentImageSerializer, DocumentPageSerializer,
DocumentSerializer, DocumentVersionSerializer)
-class APIDocumentListView(generics.ListAPIView):
+class APIDocumentListView(generics.ListCreateAPIView):
"""
Returns a list of all the documents.
"""
@@ -29,35 +33,75 @@ class APIDocumentListView(generics.ListAPIView):
serializer_class = DocumentSerializer
queryset = Document.objects.all()
- filter_backends = (MayanObjectPermissionsFilter,)
- mayan_object_permissions = [PERMISSION_DOCUMENT_VIEW]
-
-
-class APIDocumentPageView(generics.RetrieveAPIView):
- """
- Returns the selected document page details.
- """
-
- allowed_methods = ['GET']
- serializer_class = DocumentPageSerializer
- queryset = DocumentPage.objects.all()
-
permission_classes = (MayanPermission,)
- mayan_object_permissions = [PERMISSION_DOCUMENT_VIEW]
- mayan_permission_attribute_check = 'document'
+ filter_backends = (MayanObjectPermissionsFilter,)
+ mayan_object_permissions = {'GET': [PERMISSION_DOCUMENT_VIEW]}
+ mayan_view_permissions = {'POST': [PERMISSION_DOCUMENT_CREATE]}
-class APIDocumentView(generics.RetrieveAPIView):
+class APIDocumentView(generics.RetrieveUpdateDestroyAPIView):
"""
Returns the selected document details.
"""
- allowed_methods = ['GET']
serializer_class = DocumentSerializer
queryset = Document.objects.all()
permission_classes = (MayanPermission,)
- mayan_object_permissions = [PERMISSION_DOCUMENT_VIEW]
+ mayan_object_permissions = {
+ 'GET': [PERMISSION_DOCUMENT_VIEW],
+ 'PUT': [PERMISSION_DOCUMENT_PROPERTIES_EDIT],
+ 'PATCH': [PERMISSION_DOCUMENT_PROPERTIES_EDIT],
+ 'DELETE': [PERMISSION_DOCUMENT_DELETE]
+ }
+
+
+class APIDocumentVersionCreateView(generics.CreateAPIView):
+ """
+ Create a new document version.
+ """
+
+ serializer_class = DocumentVersionSerializer
+ queryset = DocumentVersion.objects.all()
+
+ permission_classes = (MayanPermission,)
+ mayan_view_permissions = {'POST': [PERMISSION_DOCUMENT_NEW_VERSION]}
+
+ def create(self, request, *args, **kwargs):
+ serializer = self.get_serializer(data=request.DATA, files=request.FILES)
+
+ if serializer.is_valid():
+ self.pre_save(serializer.object)
+ # Nested resource we take the document pk from the URL and insert it
+ # so that it needs not to be specified by the user, we mark it as
+ # a read only field in the serializer
+ serializer.object.document = get_object_or_404(Document, pk=kwargs['pk'])
+
+ try:
+ # Check the uniqueness of this version for this document instead
+ # of letting Django explode with an IntegrityError
+ DocumentVersion.objects.get(
+ document=serializer.object.document,
+ major=serializer.object.major,
+ minor=serializer.object.minor,
+ micro=serializer.object.micro,
+ release_level=serializer.object.release_level,
+ serial=serializer.object.serial
+ )
+ except DocumentVersion.DoesNotExist:
+ self.object = serializer.save(force_insert=True)
+ else:
+ return Response(
+ {'non_field_errors': 'A version with the same major, minor, micro, release_level and serial values already exist for this document.'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ self.post_save(self.object, created=True)
+ headers = self.get_success_headers(serializer.data)
+ return Response(serializer.data, status=status.HTTP_201_CREATED,
+ headers=headers)
+
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class APIDocumentVersionView(generics.RetrieveAPIView):
@@ -70,7 +114,7 @@ class APIDocumentVersionView(generics.RetrieveAPIView):
queryset = DocumentVersion.objects.all()
permission_classes = (MayanPermission,)
- mayan_object_permissions = [PERMISSION_DOCUMENT_VIEW]
+ mayan_object_permissions = {'GET': [PERMISSION_DOCUMENT_VIEW]}
mayan_permission_attribute_check = 'document'
@@ -120,3 +164,20 @@ class APIDocumentImageView(generics.GenericAPIView):
return Response({'status': 'error', 'detail': 'unknown_file_format', 'message': unicode(exception)})
except UnkownConvertError as exception:
return Response({'status': 'error', 'detail': 'converter_error', 'message': unicode(exception)})
+
+
+class APIDocumentPageView(generics.RetrieveUpdateAPIView):
+ """
+ Returns the selected document page details.
+ """
+
+ serializer_class = DocumentPageSerializer
+ queryset = DocumentPage.objects.all()
+
+ permission_classes = (MayanPermission,)
+ mayan_object_permissions = {
+ 'GET': [PERMISSION_DOCUMENT_VIEW],
+ 'PUT': [PERMISSION_DOCUMENT_EDIT],
+ 'PATCH': [PERMISSION_DOCUMENT_EDIT]
+ }
+ mayan_permission_attribute_check = 'document'
diff --git a/mayan/apps/documents/forms.py b/mayan/apps/documents/forms.py
index 015971bd31..4141a06f08 100644
--- a/mayan/apps/documents/forms.py
+++ b/mayan/apps/documents/forms.py
@@ -86,7 +86,10 @@ class DocumentPreviewForm(forms.Form):
document = kwargs.pop('document', None)
super(DocumentPreviewForm, self).__init__(*args, **kwargs)
self.fields['preview'].initial = document
- self.fields['preview'].label = _(u'Document pages (%s)') % document.pages.count()
+ try:
+ self.fields['preview'].label = _(u'Document pages (%d)') % document.pages.count()
+ except AttributeError:
+ self.fields['preview'].label = _(u'Document pages (%d)') % 0
preview = forms.CharField(widget=DocumentPagesCarouselWidget())
@@ -131,7 +134,8 @@ class DocumentForm(forms.ModelForm):
label=_(u'Quick document rename'))
if instance:
- self.version_fields(instance)
+ if instance.latest_version:
+ self.version_fields(instance)
def version_fields(self, document):
self.fields['version_update'] = forms.ChoiceField(
@@ -186,10 +190,14 @@ class DocumentForm_edit(DocumentForm):
def __init__(self, *args, **kwargs):
super(DocumentForm_edit, self).__init__(*args, **kwargs)
- self.fields.pop('serial')
- self.fields.pop('release_level')
- self.fields.pop('version_update')
- self.fields.pop('comment')
+ if kwargs['instance'].latest_version:
+ self.fields.pop('serial')
+ self.fields.pop('release_level')
+ self.fields.pop('version_update')
+ self.fields.pop('comment')
+ else:
+ self.fields.pop('new_filename')
+
self.fields.pop('use_file_name')
@@ -212,7 +220,12 @@ class DocumentContentForm(forms.Form):
super(DocumentContentForm, self).__init__(*args, **kwargs)
content = []
self.fields['contents'].initial = u''
- for page in self.document.pages.all():
+ try:
+ document_pages = self.document.pages.all()
+ except AttributeError:
+ document_pages = []
+
+ for page in document_pages:
if page.content:
content.append(conditional_escape(force_unicode(page.content)))
content.append(u'\n\n\n
- %s %s -
\n\n\n' % (ugettext(u'Page'), page.page_number))
diff --git a/mayan/apps/documents/migrations/0018_auto__chg_field_documentpage_page_label.py b/mayan/apps/documents/migrations/0018_auto__chg_field_documentpage_page_label.py
index 9c3e9d123b..17a624061e 100644
--- a/mayan/apps/documents/migrations/0018_auto__chg_field_documentpage_page_label.py
+++ b/mayan/apps/documents/migrations/0018_auto__chg_field_documentpage_page_label.py
@@ -86,6 +86,14 @@ class Migration(SchemaMigration):
'page_label': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'})
},
+ 'documents.documentpagetransformation': {
+ 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'},
+ 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['documents.DocumentPage']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
'documents.documenttype': {
'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
@@ -109,7 +117,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'major': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
- 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
@@ -143,4 +151,4 @@ class Migration(SchemaMigration):
}
}
- complete_apps = ['documents']
\ No newline at end of file
+ complete_apps = ['documents']
diff --git a/mayan/apps/documents/migrations/0019_auto__add_index_documentversion_timestamp.py b/mayan/apps/documents/migrations/0019_auto__add_index_documentversion_timestamp.py
new file mode 100644
index 0000000000..c4c3a2ee05
--- /dev/null
+++ b/mayan/apps/documents/migrations/0019_auto__add_index_documentversion_timestamp.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding index on 'DocumentVersion', fields ['timestamp']
+ db.create_index(u'documents_documentversion', ['timestamp'])
+
+
+ def backwards(self, orm):
+ # Removing index on 'DocumentVersion', fields ['timestamp']
+ db.delete_index(u'documents_documentversion', ['timestamp'])
+
+
+ models = {
+ u'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ u'auth.permission': {
+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ u'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ u'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'documents.document': {
+ 'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'},
+ 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['documents.DocumentType']", 'null': 'True', 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '48', 'blank': 'True'})
+ },
+ u'documents.documentpage': {
+ 'Meta': {'ordering': "['page_number']", 'object_name': 'DocumentPage'},
+ 'content': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'document_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['documents.DocumentVersion']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'page_label': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'page_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'})
+ },
+ u'documents.documentpagetransformation': {
+ 'Meta': {'ordering': "('order',)", 'object_name': 'DocumentPageTransformation'},
+ 'arguments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'document_page': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['documents.DocumentPage']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'transformation': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ u'documents.documenttype': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'})
+ },
+ u'documents.documenttypefilename': {
+ 'Meta': {'ordering': "['filename']", 'object_name': 'DocumentTypeFilename'},
+ 'document_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['documents.DocumentType']"}),
+ 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ u'documents.documentversion': {
+ 'Meta': {'unique_together': "(('document', 'major', 'minor', 'micro', 'release_level', 'serial'),)", 'object_name': 'DocumentVersion'},
+ 'checksum': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'versions'", 'to': u"orm['documents.Document']"}),
+ 'encoding': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'db_index': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'major': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'micro': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'minor': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'release_level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'serial': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
+ },
+ u'documents.recentdocument': {
+ 'Meta': {'ordering': "('-datetime_accessed',)", 'object_name': 'RecentDocument'},
+ 'datetime_accessed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 7, 22, 0, 0)', 'db_index': 'True'}),
+ 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['documents.Document']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+ }
+ }
+
+ complete_apps = ['documents']
\ No newline at end of file
diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py
index f3f5579719..d64b200b26 100644
--- a/mayan/apps/documents/models.py
+++ b/mayan/apps/documents/models.py
@@ -95,7 +95,11 @@ class Document(models.Model):
ordering = ['-date_added']
def __unicode__(self):
- return self.latest_version.filename
+ try:
+ return self.latest_version.filename
+ except AttributeError:
+ # Document has no version yet, let's return a place holder text
+ return ugettext(u'Uninitialized document')
@models.permalink
def get_absolute_url(self):
@@ -250,7 +254,11 @@ class Document(models.Model):
@property
def pages(self):
- return self.latest_version.pages
+ try:
+ return self.latest_version.pages
+ except AttributeError:
+ # Document has no version yet
+ return 0
@property
def page_count(self):
@@ -258,11 +266,11 @@ class Document(models.Model):
@property
def latest_version(self):
- return self.versions.order_by('-timestamp')[0]
+ return self.versions.order_by('timestamp').last()
@property
def first_version(self):
- return self.versions.order_by('timestamp')[0]
+ return self.versions.order_by('timestamp').first()
def rename(self, new_name):
version = self.latest_version
@@ -302,13 +310,13 @@ class DocumentVersion(models.Model):
def register_post_save_hook(cls, order, func):
cls._post_save_hooks[order] = func
- document = models.ForeignKey(Document, verbose_name=_(u'document'), editable=False, related_name='versions')
- major = models.PositiveIntegerField(verbose_name=_(u'mayor'), default=1, editable=False)
- minor = models.PositiveIntegerField(verbose_name=_(u'minor'), default=0, editable=False)
- micro = models.PositiveIntegerField(verbose_name=_(u'micro'), default=0, editable=False)
- release_level = models.PositiveIntegerField(choices=RELEASE_LEVEL_CHOICES, default=RELEASE_LEVEL_FINAL, verbose_name=_(u'release level'), editable=False)
- serial = models.PositiveIntegerField(verbose_name=_(u'serial'), default=0, editable=False)
- timestamp = models.DateTimeField(verbose_name=_(u'timestamp'), editable=False)
+ document = models.ForeignKey(Document, verbose_name=_(u'document'), related_name='versions')
+ major = models.PositiveIntegerField(verbose_name=_(u'mayor'), default=1)
+ minor = models.PositiveIntegerField(verbose_name=_(u'minor'), default=0)
+ micro = models.PositiveIntegerField(verbose_name=_(u'micro'), default=0)
+ release_level = models.PositiveIntegerField(choices=RELEASE_LEVEL_CHOICES, default=RELEASE_LEVEL_FINAL, verbose_name=_(u'release level'))
+ serial = models.PositiveIntegerField(verbose_name=_(u'serial'), default=0)
+ timestamp = models.DateTimeField(verbose_name=_(u'timestamp'), editable=False, db_index=True)
comment = models.TextField(blank=True, verbose_name=_(u'comment'))
# File related fields
diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py
index a1720e0eb5..a83b43b35a 100644
--- a/mayan/apps/documents/serializers.py
+++ b/mayan/apps/documents/serializers.py
@@ -11,10 +11,11 @@ class DocumentPageSerializer(serializers.HyperlinkedModelSerializer):
class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer):
- pages = DocumentPageSerializer(many=True, read_only=True)
+ pages = DocumentPageSerializer(many=True, required=False, read_only=True)
class Meta:
model = DocumentVersion
+ read_only_fields = ('document',)
class DocumentImageSerializer(serializers.Serializer):
@@ -25,6 +26,7 @@ class DocumentImageSerializer(serializers.Serializer):
class DocumentSerializer(serializers.HyperlinkedModelSerializer):
versions = DocumentVersionSerializer(many=True, read_only=True)
image = serializers.HyperlinkedIdentityField(view_name='document-image')
+ new_version = serializers.HyperlinkedIdentityField(view_name='document-new-version')
class Meta:
model = Document
diff --git a/mayan/apps/documents/tests.py b/mayan/apps/documents/tests.py
index 6c800d6700..7c1a85aeb5 100644
--- a/mayan/apps/documents/tests.py
+++ b/mayan/apps/documents/tests.py
@@ -1,5 +1,6 @@
from __future__ import absolute_import
+from json import loads
import os
from django.conf import settings
@@ -9,6 +10,9 @@ from django.core.urlresolvers import reverse
from django.test.client import Client
from django.test import TestCase
+from rest_framework import status
+from rest_framework.test import APIClient
+
from .literals import VERSION_UPDATE_MAJOR, RELEASE_LEVEL_FINAL
from .models import Document, DocumentType
@@ -95,11 +99,13 @@ class DocumentSearchTestCase(TestCase):
parse_document_page(self.document.latest_version.pages.all()[0])
def test_simple_search_after_related_name_change(self):
- from . import document_search
"""
Test that simple search works after related_name changes to
document versions and document version pages
"""
+
+ from . import document_search
+
model_list, flat_list, shown_result_count, result_count, elapsed_time = document_search.simple_search('Mayan')
self.assertEqual(result_count, 1)
self.assertEqual(flat_list, [self.document])
@@ -128,6 +134,11 @@ class DocumentSearchTestCase(TestCase):
class DocumentUploadFunctionalTestCase(TestCase):
+ """
+ Functional test to make sure all the moving parts to create a document from
+ the frontend are working correctly
+ """
+
def setUp(self):
self.admin_user = User.objects.create_superuser(username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD)
self.client = Client()
@@ -163,3 +174,73 @@ class DocumentUploadFunctionalTestCase(TestCase):
# Delete the document
response = self.client.post(reverse('document_delete', args=[self.document.pk]))
self.assertEqual(Document.objects.count(), 0)
+
+
+class DocumentAPICreateDocumentTestCase(TestCase):
+ """
+ Functional test to make sure all the moving parts to create a document from
+ the API are working correctly
+ """
+
+ def setUp(self):
+ self.admin_user = User.objects.create_superuser(username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD)
+
+ def test_uploading_a_document_using_token_auth(self):
+ # Get the an user token
+ token_client = APIClient()
+ response = token_client.post(reverse('auth_token_obtain'), {'username': TEST_ADMIN_USERNAME, 'password': TEST_ADMIN_PASSWORD})
+
+ # Be able to get authentication token
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ # Make a token was returned
+ self.assertTrue(u'token' in response.content)
+
+ token = loads(response.content)['token']
+
+ # Create a new client to simulate a different request
+ document_client = APIClient()
+
+ # Create a blank document with no token in the header
+ response = document_client.post(reverse('document-list'), {'description': 'test document'})
+
+ # Make sure toke authentication is working, should fail
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ document_client.credentials(HTTP_AUTHORIZATION='Token ' + token)
+
+ # Create a blank document
+ document_response = document_client.post(reverse('document-list'), {'description': 'test document'})
+ self.assertEqual(document_response.status_code, status.HTTP_201_CREATED)
+
+ # The document was created in the DB?
+ self.assertEqual(Document.objects.count(), 1)
+
+ new_version_url = loads(document_response.content)['new_version']
+
+ with open(TEST_DOCUMENT_PATH) as file_descriptor:
+ response = document_client.post(new_version_url, {'file': file_descriptor})
+
+ # Make sure the document uploaded correctly
+ document = Document.objects.first()
+ self.failUnlessEqual(document.exists(), True)
+ self.failUnlessEqual(document.size, 272213)
+
+ self.failUnlessEqual(document.file_mimetype, 'application/pdf')
+ self.failUnlessEqual(document.file_mime_encoding, 'binary')
+ self.failUnlessEqual(document.file_filename, 'mayan_11_1.pdf')
+ self.failUnlessEqual(document.checksum, 'c637ffab6b8bb026ed3784afdb07663fddc60099853fae2be93890852a69ecf3')
+ self.failUnlessEqual(document.page_count, 47)
+
+ # Make sure we can edit the document via the API
+ document_url = loads(document_response.content)['url']
+
+ response = document_client.post(document_url, {'description': 'edited test document'})
+
+ self.assertTrue(document.description, 'edited test document')
+
+ # Make sure we can delete the document via the API
+ response = document_client.delete(document_url)
+
+ # The document was deleted from the the DB?
+ self.assertEqual(Document.objects.count(), 0)
diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py
index b5d44a653b..9d36cc6cc4 100644
--- a/mayan/apps/documents/urls.py
+++ b/mayan/apps/documents/urls.py
@@ -3,7 +3,8 @@ from __future__ import absolute_import
from django.conf.urls import patterns, url
from .api_views import (APIDocumentView, APIDocumentImageView, APIDocumentListView,
- APIDocumentPageView, APIDocumentVersionView)
+ APIDocumentPageView, APIDocumentVersionCreateView,
+ APIDocumentVersionView)
from .conf.settings import PRINT_SIZE, DISPLAY_SIZE
urlpatterns = patterns('documents.views',
@@ -74,4 +75,5 @@ api_urls = patterns('',
url(r'^document_version/(?P[0-9]+)/$', APIDocumentVersionView.as_view(), name='documentversion-detail'),
url(r'^document_page/(?P[0-9]+)/$', APIDocumentPageView.as_view(), name='documentpage-detail'),
url(r'^documents/(?P[0-9]+)/image/$', APIDocumentImageView.as_view(), name='document-image'),
+ url(r'^documents/(?P[0-9]+)/new_version/$', APIDocumentVersionCreateView.as_view(), name='document-new-version'),
)
diff --git a/mayan/apps/documents/views.py b/mayan/apps/documents/views.py
index b7c8ad928e..ba735e6aab 100644
--- a/mayan/apps/documents/views.py
+++ b/mayan/apps/documents/views.py
@@ -96,19 +96,24 @@ def document_view(request, document_id, advanced=False):
subtemplates_list = []
if advanced:
- document_properties_form = DocumentPropertiesForm(instance=document, extra_fields=[
- {'label': _(u'Filename'), 'field': 'filename'},
- {'label': _(u'File mimetype'), 'field': lambda x: x.file_mimetype or _(u'None')},
- {'label': _(u'File mime encoding'), 'field': lambda x: x.file_mime_encoding or _(u'None')},
- {'label': _(u'File size'), 'field': lambda x: pretty_size(x.size) if x.size else '-'},
- {'label': _(u'Exists in storage'), 'field': 'exists'},
- {'label': _(u'File path in storage'), 'field': 'file'},
+ document_fields = [
{'label': _(u'Date added'), 'field': lambda x: x.date_added.date()},
{'label': _(u'Time added'), 'field': lambda x: unicode(x.date_added.time()).split('.')[0]},
- {'label': _(u'Checksum'), 'field': 'checksum'},
{'label': _(u'UUID'), 'field': 'uuid'},
- {'label': _(u'Pages'), 'field': 'page_count'},
- ])
+ ]
+ if document.latest_version:
+ document_fields.extend([
+ {'label': _(u'Filename'), 'field': 'filename'},
+ {'label': _(u'File mimetype'), 'field': lambda x: x.file_mimetype or _(u'None')},
+ {'label': _(u'File mime encoding'), 'field': lambda x: x.file_mime_encoding or _(u'None')},
+ {'label': _(u'File size'), 'field': lambda x: pretty_size(x.size) if x.size else '-'},
+ {'label': _(u'Exists in storage'), 'field': 'exists'},
+ {'label': _(u'File path in storage'), 'field': 'file'},
+ {'label': _(u'Checksum'), 'field': 'checksum'},
+ {'label': _(u'Pages'), 'field': 'page_count'},
+ ])
+
+ document_properties_form = DocumentPropertiesForm(instance=document, extra_fields=document_fields)
subtemplates_list.append(
{
@@ -235,8 +240,12 @@ def document_edit(request, document_id):
return HttpResponseRedirect(document.get_absolute_url())
else:
- form = DocumentForm_edit(instance=document, initial={
- 'new_filename': document.filename, 'description': document.description})
+ if document.latest_version:
+ form = DocumentForm_edit(instance=document, initial={
+ 'new_filename': document.filename, 'description': document.description})
+ else:
+ form = DocumentForm_edit(instance=document, initial={
+ 'description': document.description})
return render_to_response('generic_form.html', {
'form': form,
diff --git a/mayan/apps/documents/widgets.py b/mayan/apps/documents/widgets.py
index 2a6d7d7a4c..91d167f5f9 100644
--- a/mayan/apps/documents/widgets.py
+++ b/mayan/apps/documents/widgets.py
@@ -38,8 +38,12 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget):
output = []
output.append(u'')
- for page in value.pages.all():
+ try:
+ document_pages = value.pages.all()
+ except AttributeError:
+ document_pages = []
+ for page in document_pages:
output.append(u'
')
output.append(u'
%(page_string)s %(page)s
' % {'page_string': ugettext(u'Page'), 'page': page.page_number})
output.append(
@@ -77,7 +81,10 @@ def document_html_widget(document, click_view=None, page=DEFAULT_PAGE_NUMBER, zo
alt_text = _(u'document page image')
if not version:
- version = document.latest_version.pk
+ try:
+ version = document.latest_version.pk
+ except AttributeError:
+ version = None
query_dict = {
'page': page,
diff --git a/mayan/apps/installation/classes.py b/mayan/apps/installation/classes.py
index 178b2b3e46..9b0aed1c8d 100644
--- a/mayan/apps/installation/classes.py
+++ b/mayan/apps/installation/classes.py
@@ -7,7 +7,7 @@ import os
import sh
try:
- from sh import pip
+ pip = sh.Command('pip')
PIP = True
except sh.CommandNotFound:
PIP = False
diff --git a/mayan/apps/linking/views.py b/mayan/apps/linking/views.py
index 217306dedd..73540fc3ab 100644
--- a/mayan/apps/linking/views.py
+++ b/mayan/apps/linking/views.py
@@ -33,8 +33,6 @@ logger = logging.getLogger(__name__)
def smart_link_action(request):
- # Permission.objects.check_permissions(request.user, [PERMISSION_SMART_LINK_VIEW])
-
action = request.GET.get('action', None)
if not action:
@@ -178,7 +176,6 @@ def smart_link_edit(request, smart_link_pk):
form = SmartLinkForm(instance=smart_link)
return render_to_response('generic_form.html', {
- # 'navigation_object_name': 'smart_link',
'object': smart_link,
'form': form,
'title': _(u'Edit smart link: %s') % smart_link
@@ -200,10 +197,10 @@ def smart_link_delete(request, smart_link_pk):
try:
smart_link.delete()
messages.success(request, _(u'Smart link: %s deleted successfully.') % smart_link)
- except Exception, error:
- messages.error(request, _(u'Error deleting smart link: %(smart_link)s; %(error)s.') % {
+ except Exception as exception:
+ messages.error(request, _(u'Error deleting smart link: %(smart_link)s; %(exception)s.') % {
'smart_link': smart_link,
- 'error': error
+ 'exception': exception
})
return HttpResponseRedirect(next)
@@ -315,10 +312,10 @@ def smart_link_condition_delete(request, smart_link_condition_pk):
try:
smart_link_condition.delete()
messages.success(request, _(u'Smart link condition: "%s" deleted successfully.') % smart_link_condition)
- except Exception, error:
- messages.error(request, _(u'Error deleting smart link condition: %(smart_link_condition)s; %(error)s.') % {
+ except Exception as exception:
+ messages.error(request, _(u'Error deleting smart link condition: %(smart_link_condition)s; %(exception)s.') % {
'smart_link_condition': smart_link_condition,
- 'error': error
+ 'exception': exception
})
return HttpResponseRedirect(next)
diff --git a/mayan/apps/lock_manager/managers.py b/mayan/apps/lock_manager/managers.py
index bf8d5133c5..afe3bb4392 100644
--- a/mayan/apps/lock_manager/managers.py
+++ b/mayan/apps/lock_manager/managers.py
@@ -22,8 +22,8 @@ class LockManager(models.Manager):
lock.save(force_insert=True)
logger.debug('acquired lock: %s' % name)
return lock
- except IntegrityError, msg:
- logger.debug('IntegrityError: %s', msg)
+ except IntegrityError as exception:
+ logger.debug('IntegrityError: %s', exception)
# There is already an existing lock
# Check it's expiration date and if expired, reset it
try:
diff --git a/mayan/apps/metadata/forms.py b/mayan/apps/metadata/forms.py
index f2358ba0c0..6bd11a0775 100644
--- a/mayan/apps/metadata/forms.py
+++ b/mayan/apps/metadata/forms.py
@@ -1,8 +1,8 @@
from __future__ import absolute_import
from django import forms
-from django.utils.translation import ugettext_lazy as _
from django.forms.formsets import formset_factory
+from django.utils.translation import ugettext_lazy as _
from common.widgets import ScrollableCheckboxSelectMultiple
@@ -41,15 +41,15 @@ class MetadataForm(forms.Form):
choices.insert(0, ('', '------'))
self.fields['value'].choices = choices
self.fields['value'].required = required
- except Exception, err:
- self.fields['value'].initial = err
+ except Exception as exception:
+ self.fields['value'].initial = exception
self.fields['value'].widget = forms.TextInput(attrs={'readonly': 'readonly'})
if self.metadata_type.default:
try:
self.fields['value'].initial = eval(self.metadata_type.default, AVAILABLE_FUNCTIONS)
- except Exception, err:
- self.fields['value'].initial = err
+ except Exception as exception:
+ self.fields['value'].initial = exception
id = forms.CharField(label=_(u'id'), widget=forms.HiddenInput)
name = forms.CharField(label=_(u'Name'),
diff --git a/mayan/apps/navigation/templatetags/navigation_tags.py b/mayan/apps/navigation/templatetags/navigation_tags.py
index 54392ae444..fd698ddf29 100644
--- a/mayan/apps/navigation/templatetags/navigation_tags.py
+++ b/mayan/apps/navigation/templatetags/navigation_tags.py
@@ -105,9 +105,9 @@ def resolve_links(context, links, current_view, current_path, parsed_query_strin
new_link['url'] = reverse(link['view'], args=args)
if link.get('keep_query', False):
new_link['url'] = urlquote(new_link['url'], parsed_query_string)
- except NoReverseMatch, err:
+ except NoReverseMatch as exception:
new_link['url'] = '#'
- new_link['error'] = err
+ new_link['error'] = exception
elif 'url' in link:
if not link.get('dont_mark_active', False):
new_link['active'] = link['url'] == current_path
diff --git a/mayan/apps/ocr/parsers/__init__.py b/mayan/apps/ocr/parsers/__init__.py
index e158f66afa..a145ef53d4 100644
--- a/mayan/apps/ocr/parsers/__init__.py
+++ b/mayan/apps/ocr/parsers/__init__.py
@@ -116,8 +116,8 @@ class OfficeParser(Parser):
else:
raise ParserError
- except OfficeConversionError, msg:
- logger.error(msg)
+ except OfficeConversionError as exception:
+ logger.error(exception)
raise ParserError
diff --git a/mayan/apps/rest_api/__init__.py b/mayan/apps/rest_api/__init__.py
index e69de29bb2..3b434cfaea 100644
--- a/mayan/apps/rest_api/__init__.py
+++ b/mayan/apps/rest_api/__init__.py
@@ -0,0 +1,10 @@
+from __future__ import absolute_import
+
+from django.utils.translation import ugettext_lazy as _
+
+from .classes import APIEndPoint
+from .urls import api_urls
+
+endpoint = APIEndPoint('rest_api')
+endpoint.register_urls(api_urls)
+endpoint.add_endpoint('auth_token_obtain', _(u'Obtain an API authentication token.'))
diff --git a/mayan/apps/rest_api/filters.py b/mayan/apps/rest_api/filters.py
index c9eda7256d..345e12b3c3 100644
--- a/mayan/apps/rest_api/filters.py
+++ b/mayan/apps/rest_api/filters.py
@@ -10,11 +10,13 @@ from permissions.models import Permission
class MayanObjectPermissionsFilter(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
- if hasattr(view, 'mayan_object_permissions'):
+ required_permission = getattr(view, 'mayan_object_permissions', {}).get(request.method, None)
+
+ if required_permission:
try:
- Permission.objects.check_permissions(request.user, view.mayan_object_permissions)
+ Permission.objects.check_permissions(request.user, required_permission)
except PermissionDenied:
- return AccessEntry.objects.filter_objects_by_access(view.mayan_object_permissions[0], request.user, queryset)
+ return AccessEntry.objects.filter_objects_by_access(required_permission[0], request.user, queryset)
else:
return queryset
else:
diff --git a/mayan/apps/rest_api/permissions.py b/mayan/apps/rest_api/permissions.py
index 4d142b3cac..5e6c1bcaec 100644
--- a/mayan/apps/rest_api/permissions.py
+++ b/mayan/apps/rest_api/permissions.py
@@ -10,9 +10,11 @@ from permissions.models import Permission
class MayanPermission(BasePermission):
def has_permission(self, request, view):
- if hasattr(view, 'mayan_view_permissions'):
+ required_permission = getattr(view, 'mayan_view_permissions', {}).get(request.method, None)
+
+ if required_permission:
try:
- Permission.objects.check_permissions(request.user, view.mayan_view_permissions)
+ Permission.objects.check_permissions(request.user, required_permission)
except PermissionDenied:
return False
else:
@@ -21,15 +23,17 @@ class MayanPermission(BasePermission):
return True
def has_object_permission(self, request, view, obj):
- if hasattr(view, 'mayan_object_permissions'):
+ required_permission = getattr(view, 'mayan_object_permissions', {}).get(request.method, None)
+
+ if required_permission:
try:
- Permission.objects.check_permissions(request.user, view.mayan_object_permissions)
+ Permission.objects.check_permissions(request.user, required_permission)
except PermissionDenied:
try:
if hasattr(view, 'mayan_permission_attribute_check'):
- AccessEntry.objects.check_accesses(view.mayan_object_permissions, request.user, getattr(obj, view.mayan_permission_attribute_check))
+ AccessEntry.objects.check_accesses(required_permission, request.user, getattr(obj, view.mayan_permission_attribute_check))
else:
- AccessEntry.objects.check_accesses(view.mayan_object_permissions, request.user, obj)
+ AccessEntry.objects.check_accesses(required_permission, request.user, obj)
except PermissionDenied:
return False
else:
diff --git a/mayan/apps/rest_api/urls.py b/mayan/apps/rest_api/urls.py
index 2e22f0d559..f9a5d0352a 100644
--- a/mayan/apps/rest_api/urls.py
+++ b/mayan/apps/rest_api/urls.py
@@ -2,7 +2,7 @@ from __future__ import absolute_import
from django.conf.urls import include, patterns, url
-from .views import APIBase, Version_0, APIAppView
+from .views import APIBase, Version_0, APIAppView, BrowseableObtainAuthToken
version_0_urlpatterns = patterns('',
url(r'^$', Version_0.as_view(), name='api-version-0'),
@@ -13,3 +13,7 @@ urlpatterns = patterns('',
url(r'^$', APIBase.as_view(), name='api-root'),
url(r'^v0/', include(version_0_urlpatterns)),
)
+
+api_urls = patterns('',
+ url(r'^auth/token/obtain/', BrowseableObtainAuthToken.as_view(), name='auth_token_obtain'),
+)
diff --git a/mayan/apps/rest_api/views.py b/mayan/apps/rest_api/views.py
index 759b3be228..7eafb19488 100644
--- a/mayan/apps/rest_api/views.py
+++ b/mayan/apps/rest_api/views.py
@@ -3,7 +3,8 @@ from __future__ import absolute_import
import logging
-from rest_framework import generics
+from rest_framework import generics, renderers
+from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response
from rest_framework.reverse import reverse
@@ -66,3 +67,10 @@ class APIAppView(generics.GenericAPIView):
return Response({
'endpoints': result
})
+
+
+class BrowseableObtainAuthToken(ObtainAuthToken):
+ """
+ Obtain an API authentication token.
+ """
+ renderer_classes = (renderers.JSONRenderer, renderers.BrowsableAPIRenderer)
diff --git a/mayan/apps/tags/api_views.py b/mayan/apps/tags/api_views.py
index 552bb18c9a..203e08b8b5 100644
--- a/mayan/apps/tags/api_views.py
+++ b/mayan/apps/tags/api_views.py
@@ -1,12 +1,12 @@
from __future__ import absolute_import
from rest_framework import generics
+from taggit.models import Tag
from rest_api.filters import MayanObjectPermissionsFilter
from rest_api.permissions import MayanPermission
from .permissions import PERMISSION_TAG_VIEW
-from taggit.models import Tag
from .serializers import TagSerializer
@@ -18,7 +18,7 @@ class APITagView(generics.RetrieveAPIView):
queryset = Tag.objects.all()
permission_classes = (MayanPermission,)
- mayan_object_permissions = [PERMISSION_TAG_VIEW]
+ mayan_object_permissions = {'GET': [PERMISSION_TAG_VIEW]}
class APITagListView(generics.ListAPIView):
@@ -30,4 +30,4 @@ class APITagListView(generics.ListAPIView):
queryset = Tag.objects.all()
filter_backends = (MayanObjectPermissionsFilter,)
- mayan_object_permissions = [PERMISSION_TAG_VIEW]
+ mayan_object_permissions = {'GET': [PERMISSION_TAG_VIEW]}
diff --git a/mayan/settings/base.py b/mayan/settings/base.py
index 12f5e0a5dd..92c28bcec1 100644
--- a/mayan/settings/base.py
+++ b/mayan/settings/base.py
@@ -52,6 +52,7 @@ INSTALLED_APPS = (
'mptt',
'compressor',
'rest_framework',
+ 'rest_framework.authtoken',
'solo',
# Base generic
'permissions',
@@ -235,6 +236,8 @@ LOGIN_EXEMPT_URLS = (
r'^password/reset/confirm/(?P
[0-9A-Za-z]+)-(?P.+)/$',
r'^password/reset/complete/$',
r'^password/reset/done/$',
+
+ r'^api/',
)
# --------- Pagination ----------------
PAGINATION_INVALID_PAGE_RAISES_404 = True
@@ -255,6 +258,7 @@ REST_FRAMEWORK = {
'MAX_PAGINATE_BY': 100,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
+ 'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}
diff --git a/setup.py b/setup.py
index 07826433a0..9666fa72d6 100644
--- a/setup.py
+++ b/setup.py
@@ -42,13 +42,14 @@ def find_packages(directory):
os.chdir(root_dir)
for dirpath, dirnames, filenames in os.walk(directory):
- # Ignore dirnames that start with '.'
- if os.path.basename(dirpath).startswith('.'):
- continue
- if '__init__.py' in filenames:
- packages.append('.'.join(fullsplit(dirpath)))
- elif filenames:
- data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]])
+ if not dirpath.startswith('mayan/media'):
+ # Ignore dirnames that start with '.'
+ if os.path.basename(dirpath).startswith('.'):
+ continue
+ if '__init__.py' in filenames:
+ packages.append('.'.join(fullsplit(dirpath)))
+ elif filenames:
+ data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]])
return packages
install_requires = """