diff --git a/docs/releases/2.0.rst b/docs/releases/2.0.rst index 9213c5eb66..4c61286d7b 100644 --- a/docs/releases/2.0.rst +++ b/docs/releases/2.0.rst @@ -37,7 +37,11 @@ What's new in Mayan EDMS v2.0 * New document converter * New class based transformations - +* Removal of the DOCUMENT_RESTRICTIONS_OVERRIDE permission +* Use of Font awesome icons +* Move document OCR content code to the OCR app +* Add new permissions PERMISSION_OCR_CONTENT_VIEW +* Removed the page_label field Upgrading from a previous version ================================= diff --git a/mayan/apps/acls/urls.py b/mayan/apps/acls/urls.py index ba0652ae13..f3fa053817 100644 --- a/mayan/apps/acls/urls.py +++ b/mayan/apps/acls/urls.py @@ -4,19 +4,19 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'acls.views', - url(r'^new_holder_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'acl_new_holder_for', (), 'acl_new_holder_for'), - url(r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'acl_list', (), 'acl_list'), - url(r'^details/(?P[.\w]+)/holder/(?P[.\w]+)/$', 'acl_detail', (), 'acl_detail'), - url(r'^holder/new/(?P[.\w]+)/$', 'acl_holder_new', (), 'acl_holder_new'), + url(r'^new_holder_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'acl_new_holder_for', name='acl_new_holder_for'), + url(r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'acl_list', name='acl_list'), + url(r'^details/(?P[.\w]+)/holder/(?P[.\w]+)/$', 'acl_detail', name='acl_detail'), + url(r'^holder/new/(?P[.\w]+)/$', 'acl_holder_new', name='acl_holder_new'), - url(r'^multiple/grant/$', 'acl_grant', (), 'acl_multiple_grant'), - url(r'^multiple/revoke/$', 'acl_revoke', (), 'acl_multiple_revoke'), + url(r'^multiple/grant/$', 'acl_grant', name='acl_multiple_grant'), + url(r'^multiple/revoke/$', 'acl_revoke', name='acl_multiple_revoke'), - url(r'^class/$', 'acl_setup_valid_classes', (), 'acl_setup_valid_classes'), - url(r'^class/details/(?P[.\w]+)/holder/(?P[.\w]+)/$', 'acl_class_acl_detail', (), 'acl_class_acl_detail'), - url(r'^class/list_for/(?P[.\w]+)/$', 'acl_class_acl_list', (), 'acl_class_acl_list'), - url(r'^class/holder/new/(?P[.\w]+)/$', 'acl_class_new_holder_for', (), 'acl_class_new_holder_for'), + url(r'^class/$', 'acl_setup_valid_classes', name='acl_setup_valid_classes'), + url(r'^class/details/(?P[.\w]+)/holder/(?P[.\w]+)/$', 'acl_class_acl_detail', name='acl_class_acl_detail'), + url(r'^class/list_for/(?P[.\w]+)/$', 'acl_class_acl_list', name='acl_class_acl_list'), + url(r'^class/holder/new/(?P[.\w]+)/$', 'acl_class_new_holder_for', name='acl_class_new_holder_for'), - url(r'^class/multiple/grant/$', 'acl_class_multiple_grant', (), 'acl_class_multiple_grant'), - url(r'^class/multiple/revoke/$', 'acl_class_multiple_revoke', (), 'acl_class_multiple_revoke'), + url(r'^class/multiple/grant/$', 'acl_class_multiple_grant', name='acl_class_multiple_grant'), + url(r'^class/multiple/revoke/$', 'acl_class_multiple_revoke', name='acl_class_multiple_revoke'), ) diff --git a/mayan/apps/appearance/templates/appearance/base.html b/mayan/apps/appearance/templates/appearance/base.html index 7949fd07b7..4b26e5ec0d 100644 --- a/mayan/apps/appearance/templates/appearance/base.html +++ b/mayan/apps/appearance/templates/appearance/base.html @@ -101,18 +101,20 @@ @@ -182,15 +184,15 @@ {% if form_navigation_links %}
{% if form_navigation_links %} - {% with form_navigation_links as object_navigation_links %} - {% with 'true' as hide_active_anchor %} - {% with 'active' as link_class_active %} - {% with 'list-group-item btn-sm' as link_classes %} - {% include 'navigation/generic_navigation.html' %} - {% endwith %} - {% endwith %} - {% endwith %} - {% endwith %} + {% for object_navigation_links in form_navigation_links %} + {% with 'true' as hide_active_anchor %} + {% with 'active' as link_class_active %} + {% with 'list-group-item btn-sm' as link_classes %} + {% include 'navigation/generic_navigation.html' %} + {% endwith %} + {% endwith %} + {% endwith %} + {% endfor %} {% endif %}
{% endif %} diff --git a/mayan/apps/appearance/templates/appearance/generic_list_horizontal.html b/mayan/apps/appearance/templates/appearance/generic_list_horizontal.html index 6509ba9637..34527c7a36 100644 --- a/mayan/apps/appearance/templates/appearance/generic_list_horizontal.html +++ b/mayan/apps/appearance/templates/appearance/generic_list_horizontal.html @@ -13,7 +13,9 @@
{% with 'navigation/large_button_link.html' as link_template %} - {% include 'navigation/generic_navigation.html' %} + {% for object_navigation_links in resolved_links %} + {% include 'navigation/generic_navigation.html' %} + {% endfor %} {% endwith %}
diff --git a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html index 30576179a5..1982e10db0 100644 --- a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html +++ b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html @@ -51,7 +51,7 @@ {% trans 'Identifier' %} {% endif %} - {% for column in object_list.0|get_model_list_columns %} + {% for column in object_list|get_model_list_columns %} {{ column.name }} {% endfor %} @@ -101,17 +101,17 @@ {% endfor %} {% if not hide_links %} - {% get_menu_links 'object menu' source=object as links %} - {% with links as object_navigation_links %} - {% with 'true' as horizontal %} - {% include 'navigation/generic_navigation.html' %} - {% endwith %} - {% endwith %} + {% get_menu_links 'object menu' source=object as resolved_links %} + {% for object_navigation_links in resolved_links %} + {% with 'true' as horizontal %} + {% include 'navigation/generic_navigation.html' %} + {% endwith %} + {% endfor %} {% endif %} {% empty %} - {% trans 'No results' %} + {% trans 'No results' %} {% endfor %} diff --git a/mayan/apps/appearance/templates/appearance/home.html b/mayan/apps/appearance/templates/appearance/home.html index f6f128da7b..fe62f73bac 100644 --- a/mayan/apps/appearance/templates/appearance/home.html +++ b/mayan/apps/appearance/templates/appearance/home.html @@ -38,10 +38,12 @@
- {% get_menu_links 'front page menu' as object_navigation_links %} + {% get_menu_links 'front page menu' as resolved_links %} {% with 'navigation/large_button_link.html' as link_template %} {% with 'col-xs-12 col-sm-6 col-md-4 col-lg-4' as div_class %} - {% include 'navigation/generic_navigation.html' %} + {% for object_navigation_links in resolved_links %} + {% include 'navigation/generic_navigation.html' %} + {% endfor %} {% endwith %} {% endwith %}
diff --git a/mayan/apps/appearance/templates/appearance/login.html b/mayan/apps/appearance/templates/appearance/login.html index 694b74066a..42ab6e8364 100644 --- a/mayan/apps/appearance/templates/appearance/login.html +++ b/mayan/apps/appearance/templates/appearance/login.html @@ -11,8 +11,8 @@ {% block content_plain %}
- {% auto_admin_properties %} - {% if auto_admin_properties.account %} + {% autoadmin_properties %} + {% if autoadmin_properties.account %}

{% trans "First time login" %}

@@ -20,9 +20,9 @@

{% trans 'You have just finished installing Mayan EDMS, congratulations!' %}

{% trans 'Login using the following credentials:' %}

-

{% blocktrans with auto_admin_properties.account as account %}Username: {{ account }}{% endblocktrans %}

-

{% blocktrans with auto_admin_properties.account.email as email %}Email: {{ email }}{% endblocktrans %}

-

{% blocktrans with auto_admin_properties.password as password %}Password: {{ password }}{% endblocktrans %}

+

{% blocktrans with autoadmin_properties.account as account %}Username: {{ account }}{% endblocktrans %}

+

{% blocktrans with autoadmin_properties.account.email as email %}Email: {{ email }}{% endblocktrans %}

+

{% blocktrans with autoadmin_properties.password as password %}Password: {{ password }}{% endblocktrans %}

{% trans 'Be sure to change the password to increase security and to disable this message.' %}

diff --git a/mayan/apps/checkouts/api_views.py b/mayan/apps/checkouts/api_views.py index 712a8575b7..a2911acd87 100644 --- a/mayan/apps/checkouts/api_views.py +++ b/mayan/apps/checkouts/api_views.py @@ -62,7 +62,7 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView): DocumentCheckout.objects.create( document=document, expiration_datetime=timezone.localize(serializer.data['expiration_datetime']), - user_object=request.user, + user=request.user, block_new_version=serializer.data['block_new_version'] ) except Exception as exception: @@ -105,7 +105,7 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView): document = self.get_object().document - if document.checkout_info().user_object == request.user: + if document.checkout_info().user == request.user: try: Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_CHECKIN]) except PermissionDenied: diff --git a/mayan/apps/checkouts/apps.py b/mayan/apps/checkouts/apps.py index a4b1d8b8e3..a295339a19 100644 --- a/mayan/apps/checkouts/apps.py +++ b/mayan/apps/checkouts/apps.py @@ -3,14 +3,16 @@ from __future__ import absolute_import, unicode_literals from datetime import timedelta from django import apps +from django.db.models.signals import pre_save from django.utils.translation import ugettext_lazy as _ from acls.api import class_permissions from common.menus import menu_facet, menu_main, menu_sidebar -from documents.models import Document +from documents.models import Document, DocumentVersion from mayan.celery import app from rest_api.classes import APIEndPoint +from .handlers import check_if_new_versions_allowed from .links import ( link_checkin_document, link_checkout_document, link_checkout_info, link_checkout_list @@ -18,7 +20,7 @@ from .links import ( from .models import DocumentCheckout from .permissions import ( PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE, - PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE + PERMISSION_DOCUMENT_CHECKOUT ) CHECK_EXPIRED_CHECK_OUTS_INTERVAL = 60 # Lowest check out expiration allowed @@ -33,7 +35,6 @@ class CheckoutsApp(apps.AppConfig): Document.add_to_class('check_in', lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user)) Document.add_to_class('checkout_info', lambda document: DocumentCheckout.objects.document_checkout_info(document)) Document.add_to_class('checkout_state', lambda document: DocumentCheckout.objects.document_checkout_state(document)) - Document.add_to_class('is_new_versions_allowed', lambda document, user=None: DocumentCheckout.objects.is_document_new_versions_allowed(document, user)) app.conf.CELERYBEAT_SCHEDULE.update({ 'check_expired_check_outs': { @@ -47,11 +48,12 @@ class CheckoutsApp(apps.AppConfig): PERMISSION_DOCUMENT_CHECKOUT, PERMISSION_DOCUMENT_CHECKIN, PERMISSION_DOCUMENT_CHECKIN_OVERRIDE, - PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE ]) menu_facet.bind_links(links=[link_checkout_info], sources=[Document]) menu_main.bind_links(links=[link_checkout_list]) menu_sidebar.bind_links(links=[link_checkout_document, link_checkin_document], sources=['checkouts:checkout_info', 'checkouts:checkout_document', 'checkouts:checkin_document']) + pre_save.connect(check_if_new_versions_allowed, dispatch_uid='document_index_delete', sender=DocumentVersion) + APIEndPoint('checkouts') diff --git a/mayan/apps/checkouts/exceptions.py b/mayan/apps/checkouts/exceptions.py index fb17b44ae6..bd2a6b5c6e 100644 --- a/mayan/apps/checkouts/exceptions.py +++ b/mayan/apps/checkouts/exceptions.py @@ -3,16 +3,38 @@ from __future__ import unicode_literals from django.utils.translation import ugettext -class DocumentNotCheckedOut(Exception): +class DocumentCheckoutError(Exception): + """ + Base checkout exception + """ + pass + + +class DocumentCheckoutWarning(Warning): + """ + Base checkout warning + """ + pass + + +class DocumentNotCheckedOut(DocumentCheckoutError): """ Raised when trying to checkin a document that is not checkedout """ pass -class DocumentAlreadyCheckedOut(Exception): +class DocumentAlreadyCheckedOut(DocumentCheckoutError): """ Raised when trying to checkout an already checkedout document """ def __unicode__(self): return ugettext('Document already checked out.') + + +class NewDocumentVersionNotAllowed(DocumentCheckoutWarning): + """ + Uploading new versions for this document is not allowed + Current reasons: Document is in checked out state + """ + pass diff --git a/mayan/apps/checkouts/forms.py b/mayan/apps/checkouts/forms.py index b0ce0f2b0c..cda92dbee5 100644 --- a/mayan/apps/checkouts/forms.py +++ b/mayan/apps/checkouts/forms.py @@ -11,4 +11,4 @@ class DocumentCheckoutForm(forms.ModelForm): class Meta: model = DocumentCheckout - exclude = ('document', 'checkout_datetime', 'user_content_type', 'user_object_id') + exclude = ('document', 'checkout_datetime', 'user') diff --git a/mayan/apps/checkouts/handlers.py b/mayan/apps/checkouts/handlers.py new file mode 100644 index 0000000000..0f84307a8c --- /dev/null +++ b/mayan/apps/checkouts/handlers.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from .exceptions import NewDocumentVersionNotAllowed +from .models import DocumentCheckout + + +def check_if_new_versions_allowed(sender, **kwargs): + if not DocumentCheckout.objects.are_document_new_versions_allowed(kwargs['instance'].document): + raise NewDocumentVersionNotAllowed(_('New versions not allowed for the checkedout document: %s' % kwargs['instance'].document)) diff --git a/mayan/apps/checkouts/managers.py b/mayan/apps/checkouts/managers.py index e2e76648fe..422efb0862 100644 --- a/mayan/apps/checkouts/managers.py +++ b/mayan/apps/checkouts/managers.py @@ -16,12 +16,14 @@ from .events import ( ) from .exceptions import DocumentNotCheckedOut from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN -from .permissions import PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE logger = logging.getLogger(__name__) class DocumentCheckoutManager(models.Manager): + def checkout_document(self, document, expiration_datetime, user, block_new_version=True): + self.create(document=document, expiration_datetime=expiration_datetime, user=user, block_new_version=block_new_version) + def checked_out_documents(self): return Document.objects.filter(pk__in=self.model.objects.all().values_list('document__pk', flat=True)) @@ -47,7 +49,7 @@ class DocumentCheckoutManager(models.Manager): raise DocumentNotCheckedOut else: if user: - if self.document_checkout_info(document).user_object != user: + if self.document_checkout_info(document).user != user: event_document_forceful_check_in.commit(actor=user, target=document) else: event_document_check_in.commit(actor=user, target=document) @@ -68,33 +70,10 @@ class DocumentCheckoutManager(models.Manager): else: return STATE_CHECKED_IN - def is_document_new_versions_allowed(self, document, user=None): + def are_document_new_versions_allowed(self, document, user=None): try: checkout_info = self.document_checkout_info(document) except DocumentNotCheckedOut: return True else: - if not user: - return not checkout_info.block_new_version - else: - if user.is_staff or user.is_superuser: - # Allow anything to superusers and staff - return True - - if user == checkout_info.user_object: - # Allow anything to the user who checked out this document - return True - else: - # If not original user check to see if user has global or this document's PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE permission - try: - Permission.objects.check_permissions(user, [PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE]) - except PermissionDenied: - try: - AccessEntry.objects.check_accesses([PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE], user, document) - except PermissionDenied: - # Last resort check if original user enabled restriction - return not checkout_info.block_new_version - else: - return True - else: - return True + return not checkout_info.block_new_version diff --git a/mayan/apps/checkouts/migrations/0002_documentcheckout_user.py b/mayan/apps/checkouts/migrations/0002_documentcheckout_user.py new file mode 100644 index 0000000000..abd1db2b9b --- /dev/null +++ b/mayan/apps/checkouts/migrations/0002_documentcheckout_user.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('checkouts', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='documentcheckout', + name='user', + field=models.ForeignKey(verbose_name='User', blank=True, to=settings.AUTH_USER_MODEL, null=True), + preserve_default=True, + ), + ] diff --git a/mayan/apps/checkouts/migrations/0003_auto_20150617_0325.py b/mayan/apps/checkouts/migrations/0003_auto_20150617_0325.py new file mode 100644 index 0000000000..67b7945e0d --- /dev/null +++ b/mayan/apps/checkouts/migrations/0003_auto_20150617_0325.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +def move_from_content_type_user_to_foreign_key_field_user(apps, schema_editor): + # The model references the use who checked out the document using a + # generic.GenericForeignKey. This migrations changes that to a simpler + # ForeignKey to the User model + + DocumentCheckout = apps.get_model('checkouts', 'DocumentCheckout') + + for document_checkout in DocumentCheckout.objects.all(): + document_checkout.user = document_checkout.user_object + document_checkout.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('checkouts', '0002_documentcheckout_user'), + ] + + operations = [ + migrations.RunPython(move_from_content_type_user_to_foreign_key_field_user), + ] diff --git a/mayan/apps/checkouts/migrations/0004_auto_20150617_0330.py b/mayan/apps/checkouts/migrations/0004_auto_20150617_0330.py new file mode 100644 index 0000000000..f2a15bc655 --- /dev/null +++ b/mayan/apps/checkouts/migrations/0004_auto_20150617_0330.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('checkouts', '0003_auto_20150617_0325'), + ] + + operations = [ + migrations.RemoveField( + model_name='documentcheckout', + name='user_content_type', + ), + migrations.RemoveField( + model_name='documentcheckout', + name='user_object_id', + ), + migrations.AlterField( + model_name='documentcheckout', + name='user', + field=models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL), + preserve_default=True, + ), + ] diff --git a/mayan/apps/checkouts/models.py b/mayan/apps/checkouts/models.py index 83d5f4c48a..3f09fba26d 100644 --- a/mayan/apps/checkouts/models.py +++ b/mayan/apps/checkouts/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import logging +from django.conf import settings from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse @@ -27,10 +28,7 @@ class DocumentCheckout(models.Model): checkout_datetime = models.DateTimeField(verbose_name=_('Check out date and time'), auto_now_add=True) expiration_datetime = models.DateTimeField(verbose_name=_('Check out expiration date and time'), help_text=_('Amount of time to hold the document checked out in minutes.')) - # TODO: simplify user_object to an instance of User - user_content_type = models.ForeignKey(ContentType, null=True, blank=True) # blank and null added for ease of db migration - user_object_id = models.PositiveIntegerField(null=True, blank=True) - user_object = generic.GenericForeignKey(ct_field='user_content_type', fk_field='user_object_id') + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User')) block_new_version = models.BooleanField(default=True, verbose_name=_('Block new version upload'), help_text=_('Do not allow new version of this document to be uploaded.')) @@ -50,7 +48,7 @@ class DocumentCheckout(models.Model): result = super(DocumentCheckout, self).save(*args, **kwargs) if new_checkout: - event_document_check_out.commit(actor=self.user_object, target=self.document) + event_document_check_out.commit(actor=self.user, target=self.document) return result def get_absolute_url(self): diff --git a/mayan/apps/checkouts/permissions.py b/mayan/apps/checkouts/permissions.py index 5a8d631a35..3be1124d92 100644 --- a/mayan/apps/checkouts/permissions.py +++ b/mayan/apps/checkouts/permissions.py @@ -9,4 +9,3 @@ namespace = PermissionNamespace('checkouts', _('Document checkout')) PERMISSION_DOCUMENT_CHECKOUT = Permission.objects.register(namespace, 'checkout_document', _('Check out documents')) PERMISSION_DOCUMENT_CHECKIN = Permission.objects.register(namespace, 'checkin_document', _('Check in documents')) PERMISSION_DOCUMENT_CHECKIN_OVERRIDE = Permission.objects.register(namespace, 'checkin_document_override', _('Forcefully check in documents')) -PERMISSION_DOCUMENT_RESTRICTIONS_OVERRIDE = Permission.objects.register(namespace, 'checkout_restrictions_override', _('Allow overriding check out restrictions')) diff --git a/mayan/apps/checkouts/serializers.py b/mayan/apps/checkouts/serializers.py index 2c28430bf6..72f7947cca 100644 --- a/mayan/apps/checkouts/serializers.py +++ b/mayan/apps/checkouts/serializers.py @@ -15,7 +15,6 @@ class DocumentCheckoutSerializer(serializers.ModelSerializer): class Meta: model = DocumentCheckout - read_only_fields = ('user_content_type', 'user_object_id') class NewDocumentCheckoutSerializer(serializers.Serializer): diff --git a/mayan/apps/checkouts/views.py b/mayan/apps/checkouts/views.py index 82df946715..0ed610327c 100644 --- a/mayan/apps/checkouts/views.py +++ b/mayan/apps/checkouts/views.py @@ -33,7 +33,7 @@ class CheckoutListView(DocumentListView): 'title': _('Documents checked out'), 'hide_links': True, 'extra_columns': [ - {'name': _('Checkout user'), 'attribute': encapsulate(lambda document: get_object_name(document.checkout_info().user_object))}, + {'name': _('Checkout user'), 'attribute': encapsulate(lambda document: get_object_name(document.checkout_info().user))}, {'name': _('Checkout time and date'), 'attribute': encapsulate(lambda document: document.checkout_info().checkout_datetime)}, {'name': _('Checkout expiration'), 'attribute': encapsulate(lambda document: document.checkout_info().expiration_datetime)}, ], @@ -51,7 +51,7 @@ def checkout_info(request, document_pk): if document.is_checked_out(): checkout_info = document.checkout_info() - paragraphs.append(_('User: %s') % get_object_name(checkout_info.user_object)) + paragraphs.append(_('User: %s') % get_object_name(checkout_info.user)) paragraphs.append(_('Check out time: %s') % checkout_info.checkout_datetime) paragraphs.append(_('Check out expiration: %s') % checkout_info.expiration_datetime) paragraphs.append(_('New versions allowed: %s') % (_('Yes') if not checkout_info.block_new_version else _('No'))) @@ -74,10 +74,12 @@ def checkout_document(request, document_pk): form = DocumentCheckoutForm(data=request.POST, initial={'document': document}) if form.is_valid(): try: - document_checkout = form.save(commit=False) - document_checkout.user_object = request.user - document_checkout.document = document - document_checkout.save() + DocumentCheckout.objects.checkout_document( + document=document, + expiration_datetime=form.cleaned_data['expiration_datetime'], + user=request.user, + block_new_version=form.cleaned_data['block_new_version'], + ) except DocumentAlreadyCheckedOut: messages.error(request, _('Document already checked out.')) return HttpResponseRedirect(reverse('checkouts:checkout_info', args=[document.pk])) @@ -110,7 +112,7 @@ def checkin_document(request, document_pk): # If the user trying to check in the document is the same as the check out # user just check for the normal permission otherwise check for the forceful # checkin permission - if document.checkout_info().user_object == request.user: + if document.checkout_info().user == request.user: try: Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_CHECKIN]) except PermissionDenied: @@ -139,7 +141,7 @@ def checkin_document(request, document_pk): 'object': document, } - if document.checkout_info().user_object != request.user: + if document.checkout_info().user != request.user: context['title'] = _('You didn\'t originally checked out this document. Are you sure you wish to forcefully check in document: %s?') % document else: context['title'] = _('Are you sure you wish to check in document: %s?') % document diff --git a/mayan/apps/common/admin.py b/mayan/apps/common/admin.py index a5ad7b9106..4347d84e34 100644 --- a/mayan/apps/common/admin.py +++ b/mayan/apps/common/admin.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import AutoAdminSingleton, SharedUploadedFile +from .models import SharedUploadedFile -admin.site.register(AutoAdminSingleton) admin.site.register(SharedUploadedFile) diff --git a/mayan/apps/common/apps.py b/mayan/apps/common/apps.py index 1543771f24..cbd09140a2 100644 --- a/mayan/apps/common/apps.py +++ b/mayan/apps/common/apps.py @@ -5,8 +5,6 @@ import tempfile from django import apps from django.conf import settings -from django.contrib.auth import models as auth_models -from django.contrib.auth.models import User from django.contrib.auth.signals import user_logged_in from django.db.models.signals import post_migrate, post_save from django.utils.translation import ugettext_lazy as _ @@ -14,8 +12,7 @@ from django.utils.translation import ugettext_lazy as _ from common import settings as common_settings from .handlers import ( - auto_admin_account_passwd_change, user_locale_profile_session_config, - user_locale_profile_create + user_locale_profile_session_config, user_locale_profile_create ) from .links import ( link_about, link_admin_site, link_current_user_details, @@ -26,48 +23,15 @@ from .links import ( from .menus import ( menu_facet, menu_main, menu_secondary, menu_setup, menu_tools ) -from .models import ( - AnonymousUserSingleton, AutoAdminSingleton, UserLocaleProfile -) -from .settings import ( - AUTO_ADMIN_USERNAME, AUTO_ADMIN_PASSWORD, AUTO_CREATE_ADMIN, - TEMPORARY_DIRECTORY -) +from .models import AnonymousUserSingleton +from .settings import TEMPORARY_DIRECTORY from .utils import validate_path logger = logging.getLogger(__name__) -def create_superuser_and_anonymous_user(sender, **kwargs): - """ - From https://github.com/lambdalisue/django-qwert/blob/master/qwert/autoscript/__init__.py - From http://stackoverflow.com/questions/1466827/ -- - - Prevent interactive question about wanting a superuser created. (This code - has to go in this otherwise empty "models" module so that it gets processed by - the "syncdb" command during database creation.) - - Create our own admin super user automatically. - """ - if kwargs['app_config'].__class__ == CommonApp: - AutoAdminSingleton.objects.get_or_create() - AnonymousUserSingleton.objects.get_or_create() - - if AUTO_CREATE_ADMIN: - try: - auth_models.User.objects.get(username=AUTO_ADMIN_USERNAME) - except auth_models.User.DoesNotExist: - logger.info('Creating super admin user -- login: %s, password: %s', AUTO_ADMIN_USERNAME, AUTO_ADMIN_PASSWORD) - assert auth_models.User.objects.create_superuser(AUTO_ADMIN_USERNAME, 'autoadmin@autoadmin.com', AUTO_ADMIN_PASSWORD) - admin = auth_models.User.objects.get(username=AUTO_ADMIN_USERNAME) - # Store the auto admin password properties to display the first login message - auto_admin_properties, created = AutoAdminSingleton.objects.get_or_create() - auto_admin_properties.account = admin - auto_admin_properties.password = AUTO_ADMIN_PASSWORD - auto_admin_properties.password_hash = admin.password - auto_admin_properties.save() - else: - logger.info('Super admin user already exists. -- login: %s', AUTO_ADMIN_USERNAME) +def create_anonymous_user(sender, **kwargs): + AnonymousUserSingleton.objects.get_or_create() class CommonApp(apps.AppConfig): @@ -90,10 +54,9 @@ class CommonApp(apps.AppConfig): menu_setup.bind_links(links=[link_admin_site]) menu_tools.bind_links(links=[link_maintenance_menu]) - post_migrate.connect(create_superuser_and_anonymous_user, dispatch_uid='create_superuser_and_anonymous_user') - post_save.connect(auto_admin_account_passwd_change, dispatch_uid='auto_admin_account_passwd_change', sender=User) - user_logged_in.connect(user_locale_profile_session_config, dispatch_uid='user_locale_profile_session_config', sender=User) - post_save.connect(user_locale_profile_create, dispatch_uid='user_locale_profile_create', sender=User) + post_migrate.connect(create_anonymous_user, dispatch_uid='create_anonymous_user', sender=self) + user_logged_in.connect(user_locale_profile_session_config, dispatch_uid='user_locale_profile_session_config', sender=settings.AUTH_USER_MODEL) + post_save.connect(user_locale_profile_create, dispatch_uid='user_locale_profile_create', sender=settings.AUTH_USER_MODEL) if (not validate_path(TEMPORARY_DIRECTORY)) or (not TEMPORARY_DIRECTORY): setattr(common_settings, 'TEMPORARY_DIRECTORY', tempfile.mkdtemp()) diff --git a/mayan/apps/common/classes.py b/mayan/apps/common/classes.py index 48a576efb1..17c6fec4a5 100644 --- a/mayan/apps/common/classes.py +++ b/mayan/apps/common/classes.py @@ -83,4 +83,3 @@ class MissingItem(object): self.description = description self.view = view self.__class__._registry.append(self) - diff --git a/mayan/apps/common/forms.py b/mayan/apps/common/forms.py index 376d51957e..e168b6bdf3 100644 --- a/mayan/apps/common/forms.py +++ b/mayan/apps/common/forms.py @@ -5,7 +5,6 @@ import os from django import forms from django.conf import settings -from django.contrib.auth import authenticate from django.contrib.auth.models import User from django.db import models from django.utils.html import escape @@ -13,7 +12,7 @@ from django.utils.translation import ugettext_lazy as _ from .models import UserLocaleProfile from .utils import return_attrib -from .widgets import DetailSelectMultiple, EmailInput, PlainWidget +from .widgets import DetailSelectMultiple, PlainWidget class DetailForm(forms.ModelForm): diff --git a/mayan/apps/common/handlers.py b/mayan/apps/common/handlers.py index 94bc9df528..798101c7d6 100644 --- a/mayan/apps/common/handlers.py +++ b/mayan/apps/common/handlers.py @@ -2,19 +2,7 @@ from __future__ import unicode_literals from django.conf import settings -from .models import ( - AnonymousUserSingleton, AutoAdminSingleton, UserLocaleProfile -) - - -def auto_admin_account_passwd_change(sender, instance, **kwargs): - auto_admin_properties = AutoAdminSingleton.objects.get() - if instance == auto_admin_properties.account and instance.password != auto_admin_properties.password_hash: - # Only delete the auto admin properties when the password has been changed - auto_admin_properties.account = None - auto_admin_properties.password = None - auto_admin_properties.password_hash = None - auto_admin_properties.save() +from .models import UserLocaleProfile def user_locale_profile_session_config(sender, request, user, **kwargs): diff --git a/mayan/apps/common/management/commands/initialsetup.py b/mayan/apps/common/management/commands/initialsetup.py index 95812b797c..4c68772042 100644 --- a/mayan/apps/common/management/commands/initialsetup.py +++ b/mayan/apps/common/management/commands/initialsetup.py @@ -6,11 +6,14 @@ from django.conf import settings from django.core import management from django.utils.crypto import get_random_string +from ...signals import post_initial_setup + class Command(management.BaseCommand): help = 'Gets Mayan EDMS ready to be used (initializes database, creates a secret key, etc).' - def _generate_secret_key(self): + @staticmethod + def _generate_secret_key(): chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' return get_random_string(50, chars) @@ -26,7 +29,9 @@ class Command(management.BaseCommand): '', 'from .base import *', '', - "SECRET_KEY = '{0}'".format(self._generate_secret_key()), + "SECRET_KEY = '{0}'".format(Command._generate_secret_key()), '', ])) - management.call_command('syncdb', migrate=True, interactive=False) + management.call_command('migrate', interactive=False) + management.call_command('createautoadmin', interactive=False) + post_initial_setup.send(sender=self) diff --git a/mayan/apps/common/migrations/0003_auto_20150614_0723.py b/mayan/apps/common/migrations/0003_auto_20150614_0723.py new file mode 100644 index 0000000000..438d705a9f --- /dev/null +++ b/mayan/apps/common/migrations/0003_auto_20150614_0723.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0002_auto_20150608_1902'), + ] + + operations = [ + migrations.RemoveField( + model_name='autoadminsingleton', + name='account', + ), + migrations.DeleteModel( + name='AutoAdminSingleton', + ), + ] diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 58559ddf05..670111929e 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -29,7 +29,7 @@ class ObjectListPermissionFilterMixin(object): if self.object_permission: try: # Check to see if the user has the permissions globally - Permission.objects.check_permissions(self.request.user, [self.object_permission]) + Permission.objects.check_permissions(self.request.user, (self.object_permission,)) except PermissionDenied: # No global permission, filter ther queryset per object + permission return AccessEntry.objects.filter_objects_by_access(self.object_permission, self.request.user, queryset) @@ -43,13 +43,16 @@ class ObjectListPermissionFilterMixin(object): class ObjectPermissionCheckMixin(object): object_permission = None + def get_permission_object(self): + return self.get_object() + def dispatch(self, request, *args, **kwargs): if self.object_permission: try: - Permission.objects.check_permissions(request.user, [self.object_permission]) + Permission.objects.check_permissions(request.user, (self.object_permission,)) except PermissionDenied: - AccessEntry.objects.check_access(self.object_permission, request.user, self.get_object()) + AccessEntry.objects.check_access(self.object_permission, request.user, self.get_permission_object()) return super(ObjectPermissionCheckMixin, self).dispatch(request, *args, **kwargs) @@ -80,6 +83,6 @@ class ViewPermissionCheckMixin(object): def dispatch(self, request, *args, **kwargs): if self.view_permission: - Permission.objects.check_permissions(self.request.user, [self.view_permission]) + Permission.objects.check_permissions(self.request.user, (self.view_permission,)) return super(ViewPermissionCheckMixin, self).dispatch(request, *args, **kwargs) diff --git a/mayan/apps/common/models.py b/mayan/apps/common/models.py index 1300e7e3bc..7fafed0bca 100644 --- a/mayan/apps/common/models.py +++ b/mayan/apps/common/models.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals from pytz import common_timezones from django.conf import settings -from django.contrib.auth.models import User from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _, ugettext @@ -32,15 +31,6 @@ class AnonymousUserSingleton(SingletonModel): verbose_name = verbose_name_plural = _('Anonymous user') -class AutoAdminSingleton(SingletonModel): - account = models.ForeignKey(User, null=True, blank=True, related_name='auto_admin_account', verbose_name=_('Account')) - password = models.CharField(null=True, blank=True, verbose_name=_('Password'), max_length=128) - password_hash = models.CharField(null=True, blank=True, verbose_name=_('Password hash'), max_length=128) - - class Meta: - verbose_name = verbose_name_plural = _('Auto admin properties') - - @python_2_unicode_compatible class SharedUploadedFile(models.Model): file = models.FileField(upload_to=upload_to, storage=shared_storage_backend, verbose_name=_('File')) @@ -61,7 +51,7 @@ class SharedUploadedFile(models.Model): @python_2_unicode_compatible class UserLocaleProfile(models.Model): - user = models.OneToOneField(User, related_name='locale_profile', verbose_name=_('User')) + user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='locale_profile', verbose_name=_('User')) timezone = models.CharField(choices=zip(common_timezones, common_timezones), max_length=48, verbose_name=_('Timezone')) language = models.CharField(choices=settings.LANGUAGES, max_length=8, verbose_name=_('Language')) diff --git a/mayan/apps/common/settings.py b/mayan/apps/common/settings.py index b7ed5a434a..6b962637eb 100644 --- a/mayan/apps/common/settings.py +++ b/mayan/apps/common/settings.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ from smart_settings.api import register_setting @@ -15,30 +14,6 @@ TEMPORARY_DIRECTORY = register_setting( exists=True ) -register_setting( - namespace='common', - module='common.settings', - name='AUTO_CREATE_ADMIN', - global_name='COMMON_AUTO_CREATE_ADMIN', - default=True, -) - -register_setting( - namespace='common', - module='common.settings', - name='AUTO_ADMIN_USERNAME', - global_name='COMMON_AUTO_ADMIN_USERNAME', - default='admin', -) - -register_setting( - namespace='common', - module='common.settings', - name='AUTO_ADMIN_PASSWORD', - global_name='COMMON_AUTO_ADMIN_PASSWORD', - default=User.objects.make_random_password(), -) - register_setting( namespace='common', module='common.settings', diff --git a/mayan/apps/common/signals.py b/mayan/apps/common/signals.py new file mode 100644 index 0000000000..8b91f2b550 --- /dev/null +++ b/mayan/apps/common/signals.py @@ -0,0 +1,5 @@ +from __future__ import unicode_literals + +from django.dispatch import Signal + +post_initial_setup = Signal(use_caching=True) diff --git a/mayan/apps/common/templatetags/attribute_tags.py b/mayan/apps/common/templatetags/attribute_tags.py index e19390d901..2cd00fe82b 100644 --- a/mayan/apps/common/templatetags/attribute_tags.py +++ b/mayan/apps/common/templatetags/attribute_tags.py @@ -13,8 +13,23 @@ def object_property(value, arg): @register.filter def get_model_list_columns(obj): + try: + # Is it a query set? + obj = obj.model + except AttributeError: + # Is not a query set + try: + # Is iterable? + obj = obj[0] + except TypeError: + # It is not + pass + except IndexError: + # It a list and it's empty + pass + for key, value in model_list_columns.items(): - if isinstance(obj, key): + if key == obj or isinstance(obj, key): return value return [] diff --git a/mayan/apps/common/templatetags/project_tags.py b/mayan/apps/common/templatetags/project_tags.py index b7f2c87be5..b7eed36fb0 100644 --- a/mayan/apps/common/templatetags/project_tags.py +++ b/mayan/apps/common/templatetags/project_tags.py @@ -14,4 +14,3 @@ def project_name(): @register.simple_tag def project_version(): return mayan.__version__ - diff --git a/mayan/apps/common/templatetags/subtemplates_tags.py b/mayan/apps/common/templatetags/subtemplates_tags.py index f253c76ff0..3da020b044 100644 --- a/mayan/apps/common/templatetags/subtemplates_tags.py +++ b/mayan/apps/common/templatetags/subtemplates_tags.py @@ -16,4 +16,3 @@ def render_subtemplate(context, template_name, template_context): new_context = Context(context) new_context.update(Context(template_context)) return get_template(template_name).render(new_context) - diff --git a/mayan/apps/common/utils.py b/mayan/apps/common/utils.py index 41bbd5a131..50706d1239 100644 --- a/mayan/apps/common/utils.py +++ b/mayan/apps/common/utils.py @@ -11,8 +11,6 @@ from django.contrib.contenttypes.models import ContentType from django.utils.datastructures import MultiValueDict from django.utils.http import urlquote as django_urlquote from django.utils.http import urlencode as django_urlencode -from django.utils.importlib import import_module -from django.utils.translation import ugettext_lazy as _ logger = logging.getLogger(__name__) @@ -87,7 +85,6 @@ def get_descriptor(file_input, read=True): def get_object_name(obj): - ct_label = ContentType.objects.get_for_model(obj).name if isinstance(obj, User): label = obj.get_full_name() if obj.get_full_name() else obj else: diff --git a/mayan/apps/common/views.py b/mayan/apps/common/views.py index db03ad9570..d7165420bb 100644 --- a/mayan/apps/common/views.py +++ b/mayan/apps/common/views.py @@ -5,12 +5,11 @@ from json import dumps, loads from django.conf import settings from django.contrib import messages from django.contrib.auth.models import User -from django.contrib.auth.views import login, password_change from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect -from django.shortcuts import redirect, render_to_response +from django.http import Http404, HttpResponseRedirect +from django.shortcuts import render_to_response from django.template import RequestContext from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ @@ -20,7 +19,6 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.list import ListView from dynamic_search.classes import SearchModel -from permissions.models import Permission from .api import tools from .classes import MissingItem @@ -33,7 +31,6 @@ from .mixins import ( ExtraContextMixin, ObjectListPermissionFilterMixin, ObjectPermissionCheckMixin, RedirectionMixin, ViewPermissionCheckMixin ) -from .utils import return_attrib class AssignRemoveView(TemplateView): @@ -138,6 +135,10 @@ class AboutView(TemplateView): template_name = 'appearance/about.html' +class ConfirmView(ObjectListPermissionFilterMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, TemplateView): + template_name = 'appearance/generic_confirm.html' + + class CurrentUserDetailsView(TemplateView): template_name = 'appearance/generic_form.html' @@ -294,6 +295,8 @@ class MultiFormView(FormView): return self.forms_invalid(forms) +# TODO: check/test if ViewPermissionCheckMixin, ObjectPermissionCheckMixin are +# in the right MRO class SingleObjectEditView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, UpdateView): template_name = 'appearance/generic_form.html' @@ -395,7 +398,7 @@ class ParentChildListView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, is_empty = len(self.object_list) == 0 if is_empty: raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.") - % {'class_name': self.__class__.__name__}) + % {'class_name': self.__class__.__name__}) context = self.get_context_data(object=self.object) return self.render_to_response(context) @@ -456,7 +459,7 @@ class SetupListView(TemplateView): def get_context_data(self, **kwargs): data = super(SetupListView, self).get_context_data(**kwargs) data.update({ - 'object_navigation_links': menu_setup.resolve(context=RequestContext(self.request)), + 'resolved_links': menu_setup.resolve(context=RequestContext(self.request)), 'title': _('Setup items'), }) return data @@ -468,7 +471,7 @@ class ToolsListView(TemplateView): def get_context_data(self, **kwargs): data = super(ToolsListView, self).get_context_data(**kwargs) data.update({ - 'object_navigation_links': menu_tools.resolve(context=RequestContext(self.request)), + 'resolved_links': menu_tools.resolve(context=RequestContext(self.request)), 'title': _('Tools'), }) return data diff --git a/mayan/apps/common/widgets.py b/mayan/apps/common/widgets.py index 6d3ca56743..2bdd5a8a03 100644 --- a/mayan/apps/common/widgets.py +++ b/mayan/apps/common/widgets.py @@ -11,15 +11,6 @@ from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ -class PlainWidget(forms.widgets.Widget): - """ - Class to define a form widget that effectively nulls the htmls of a - widget and reduces the output to only it's value - """ - def render(self, name, value, attrs=None): - return mark_safe('%s' % value) - - class DetailSelectMultiple(forms.widgets.SelectMultiple): def __init__(self, queryset=None, *args, **kwargs): self.queryset = queryset @@ -61,43 +52,6 @@ class DetailSelectMultiple(forms.widgets.SelectMultiple): return mark_safe(output + '\n') -def exists_widget(path): - try: - return two_state_template(os.path.exists(path)) - except Exception as exception: - return exception - - -def two_state_template(state, ok_icon='fa fa-check', fail_icon='fa fa-times'): - if state: - return mark_safe(''.format(ok_icon)) - else: - return mark_safe(''.format(fail_icon)) - - -class TextAreaDiv(forms.widgets.Widget): - """ - Class to define a form widget that simulates the behavior of a - Textarea widget but using a div tag instead - """ - - def __init__(self, attrs=None): - # The 'rows' and 'cols' attributes are required for HTML correctness. - default_attrs = {'class': 'text_area_div'} - if attrs: - default_attrs.update(attrs) - super(TextAreaDiv, self).__init__(default_attrs) - - def render(self, name, value, attrs=None): - if value is None: - value = '' - - flat_attrs = flatatt(self.build_attrs(attrs, name=name)) - content = conditional_escape(force_unicode(value)) - result = '%s' % (flat_attrs, content) - return mark_safe(result) - - # From: http://www.peterbe.com/plog/emailinput-html5-django class EmailInput(forms.widgets.Input): """ @@ -115,6 +69,15 @@ class EmailInput(forms.widgets.Input): return super(EmailInput, self).render(name, value, attrs=attrs) +class PlainWidget(forms.widgets.Widget): + """ + Class to define a form widget that effectively nulls the htmls of a + widget and reduces the output to only it's value + """ + def render(self, name, value, attrs=None): + return mark_safe('%s' % value) + + class ScrollableCheckboxSelectMultiple(forms.widgets.CheckboxSelectMultiple): """ Class for a form widget composed of a selection of checkboxes wrapped @@ -146,3 +109,40 @@ class ScrollableCheckboxSelectMultiple(forms.widgets.CheckboxSelectMultiple): output.append('') return mark_safe('
%s
' % '\n'.join(output)) + + +class TextAreaDiv(forms.widgets.Widget): + """ + Class to define a form widget that simulates the behavior of a + Textarea widget but using a div tag instead + """ + + def __init__(self, attrs=None): + # The 'rows' and 'cols' attributes are required for HTML correctness. + default_attrs = {'class': 'text_area_div'} + if attrs: + default_attrs.update(attrs) + super(TextAreaDiv, self).__init__(default_attrs) + + def render(self, name, value, attrs=None): + if value is None: + value = '' + + flat_attrs = flatatt(self.build_attrs(attrs, name=name)) + content = conditional_escape(force_unicode(value)) + result = '%s' % (flat_attrs, content) + return mark_safe(result) + + +def exists_widget(path): + try: + return two_state_template(os.path.exists(path)) + except Exception as exception: + return exception + + +def two_state_template(state, ok_icon='fa fa-check', fail_icon='fa fa-times'): + if state: + return mark_safe(''.format(ok_icon)) + else: + return mark_safe(''.format(fail_icon)) diff --git a/mayan/apps/converter/urls.py b/mayan/apps/converter/urls.py index 60af32b529..195656d0d1 100644 --- a/mayan/apps/converter/urls.py +++ b/mayan/apps/converter/urls.py @@ -4,8 +4,8 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'converter.views', - url(r'^create_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'transformation_create', (), 'transformation_create'), - url(r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'transformation_list', (), 'transformation_list'), - url(r'^delete/(?P\d+)/$', 'transformation_delete', (), 'transformation_delete'), - url(r'^edit/(?P\d+)/$', 'transformation_edit', (), 'transformation_edit'), + url(r'^create_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'transformation_create', name='transformation_create'), + url(r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'transformation_list', name='transformation_list'), + url(r'^delete/(?P\d+)/$', 'transformation_delete', name='transformation_delete'), + url(r'^edit/(?P\d+)/$', 'transformation_edit', name='transformation_edit'), ) diff --git a/mayan/apps/django_gpg/urls.py b/mayan/apps/django_gpg/urls.py index dc172041a4..39ed308180 100644 --- a/mayan/apps/django_gpg/urls.py +++ b/mayan/apps/django_gpg/urls.py @@ -2,9 +2,9 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'django_gpg.views', - url(r'^delete/(?P.+)/(?P\w+)/$', 'key_delete', (), 'key_delete'), + url(r'^delete/(?P.+)/(?P\w+)/$', 'key_delete', name='key_delete'), url(r'^list/private/$', 'key_list', {'secret': True}, 'key_private_list'), url(r'^list/public/$', 'key_list', {'secret': False}, 'key_public_list'), - url(r'^query/$', 'key_query', (), 'key_query'), - url(r'^receive/(?P.+)/$', 'key_receive', (), 'key_receive'), + url(r'^query/$', 'key_query', name='key_query'), + url(r'^receive/(?P.+)/$', 'key_receive', name='key_receive'), ) diff --git a/mayan/apps/document_comments/urls.py b/mayan/apps/document_comments/urls.py index 2d2f2cae2f..188ca85d72 100644 --- a/mayan/apps/document_comments/urls.py +++ b/mayan/apps/document_comments/urls.py @@ -4,8 +4,8 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'document_comments.views', - url(r'^comment/(?P\d+)/delete/$', 'comment_delete', (), 'comment_delete'), - url(r'^comment/multiple/delete/$', 'comment_multiple_delete', (), 'comment_multiple_delete'), - url(r'^(?P\d+)/comment/add/$', 'comment_add', (), 'comment_add'), - url(r'^(?P\d+)/comment/list/$', 'comments_for_document', (), 'comments_for_document'), + url(r'^comment/(?P\d+)/delete/$', 'comment_delete', name='comment_delete'), + url(r'^comment/multiple/delete/$', 'comment_multiple_delete', name='comment_multiple_delete'), + url(r'^(?P\d+)/comment/add/$', 'comment_add', name='comment_add'), + url(r'^(?P\d+)/comment/list/$', 'comments_for_document', name='comments_for_document'), ) diff --git a/mayan/apps/document_indexing/links.py b/mayan/apps/document_indexing/links.py index 3a3af6bb1a..b15b93e95b 100644 --- a/mayan/apps/document_indexing/links.py +++ b/mayan/apps/document_indexing/links.py @@ -23,9 +23,9 @@ def is_not_root_node(context): link_document_index_list = Link(permissions=[PERMISSION_DOCUMENT_INDEXING_VIEW, PERMISSION_DOCUMENT_VIEW], text=_('Indexes'), view='indexing:document_index_list', args='object.pk') link_index_list = Link(permissions=[PERMISSION_DOCUMENT_INDEXING_VIEW], text=_('Index list'), view='indexing:index_list') -link_index_main_menu = Link(icon='fa fa-sitemap', text=_('Indexes'), view='indexing:index_list') +link_index_main_menu = Link(icon='fa fa-list-ul', text=_('Indexes'), view='indexing:index_list') link_index_parent = Link(condition=is_not_instance_root_node, permissions=[PERMISSION_DOCUMENT_INDEXING_VIEW], text=_('Go up one level'), view='indexing:index_instance_node_view', args='object.parent.pk') -link_index_setup = Link(icon='fa fa-sitemap', permissions=[PERMISSION_DOCUMENT_INDEXING_SETUP], text=_('Indexes'), view='indexing:index_setup_list') +link_index_setup = Link(icon='fa fa-list-ul', permissions=[PERMISSION_DOCUMENT_INDEXING_SETUP], text=_('Indexes'), view='indexing:index_setup_list') link_index_setup_list = Link(permissions=[PERMISSION_DOCUMENT_INDEXING_SETUP], text=_('Indexes'), view='indexing:index_setup_list') link_index_setup_create = Link(permissions=[PERMISSION_DOCUMENT_INDEXING_CREATE], text=_('Create index'), view='indexing:index_setup_create') link_index_setup_edit = Link(permissions=[PERMISSION_DOCUMENT_INDEXING_EDIT], text=_('Edit'), view='indexing:index_setup_edit', args='resolved_object.pk') diff --git a/mayan/apps/document_indexing/urls.py b/mayan/apps/document_indexing/urls.py index 8b10297642..bf4e4a4839 100644 --- a/mayan/apps/document_indexing/urls.py +++ b/mayan/apps/document_indexing/urls.py @@ -12,22 +12,22 @@ from .views import SetupIndexDocumentTypesView urlpatterns = patterns( 'document_indexing.views', - url(r'^setup/index/list/$', 'index_setup_list', (), 'index_setup_list'), - url(r'^setup/index/create/$', 'index_setup_create', (), 'index_setup_create'), - url(r'^setup/index/(?P\d+)/edit/$', 'index_setup_edit', (), 'index_setup_edit'), - url(r'^setup/index/(?P\d+)/delete/$', 'index_setup_delete', (), 'index_setup_delete'), - url(r'^setup/index/(?P\d+)/view/$', 'index_setup_view', (), 'index_setup_view'), + url(r'^setup/index/list/$', 'index_setup_list', name='index_setup_list'), + url(r'^setup/index/create/$', 'index_setup_create', name='index_setup_create'), + url(r'^setup/index/(?P\d+)/edit/$', 'index_setup_edit', name='index_setup_edit'), + url(r'^setup/index/(?P\d+)/delete/$', 'index_setup_delete', name='index_setup_delete'), + url(r'^setup/index/(?P\d+)/view/$', 'index_setup_view', name='index_setup_view'), url(r'^setup/index/(?P\d+)/document_types/$', SetupIndexDocumentTypesView.as_view(), name='index_setup_document_types'), - url(r'^setup/template/node/(?P\d+)/create/child/$', 'template_node_create', (), 'template_node_create'), - url(r'^setup/template/node/(?P\d+)/edit/$', 'template_node_edit', (), 'template_node_edit'), - url(r'^setup/template/node/(?P\d+)/delete/$', 'template_node_delete', (), 'template_node_delete'), + url(r'^setup/template/node/(?P\d+)/create/child/$', 'template_node_create', name='template_node_create'), + url(r'^setup/template/node/(?P\d+)/edit/$', 'template_node_edit', name='template_node_edit'), + url(r'^setup/template/node/(?P\d+)/delete/$', 'template_node_delete', name='template_node_delete'), - url(r'^index/list/$', 'index_list', (), 'index_list'), - url(r'^instance/node/(?P\d+)/$', 'index_instance_node_view', (), 'index_instance_node_view'), + url(r'^index/list/$', 'index_list', name='index_list'), + url(r'^instance/node/(?P\d+)/$', 'index_instance_node_view', name='index_instance_node_view'), - url(r'^rebuild/all/$', 'rebuild_index_instances', (), 'rebuild_index_instances'), - url(r'^list/for/document/(?P\d+)/$', 'document_index_list', (), 'document_index_list'), + url(r'^rebuild/all/$', 'rebuild_index_instances', name='rebuild_index_instances'), + url(r'^list/for/document/(?P\d+)/$', 'document_index_list', name='document_index_list'), ) api_urls = patterns( diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 3651d40107..f2b4e0c87d 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -4,8 +4,8 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'document_signatures.views', - url(r'^verify/(?P\d+)/$', 'document_verify', (), 'document_verify'), - url(r'^upload/signature/(?P\d+)/$', 'document_signature_upload', (), 'document_signature_upload'), - url(r'^download/signature/(?P\d+)/$', 'document_signature_download', (), 'document_signature_download'), - url(r'^document/(?P\d+)/signature/delete/$', 'document_signature_delete', (), 'document_signature_delete'), + url(r'^verify/(?P\d+)/$', 'document_verify', name='document_verify'), + url(r'^upload/signature/(?P\d+)/$', 'document_signature_upload', name='document_signature_upload'), + url(r'^download/signature/(?P\d+)/$', 'document_signature_download', name='document_signature_download'), + url(r'^document/(?P\d+)/signature/delete/$', 'document_signature_delete', name='document_signature_delete'), ) diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index f2c8ce61d1..e822232f0f 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -66,6 +66,10 @@ class DocumentStatesApp(apps.AppConfig): 'name': _('Date and time'), 'attribute': encapsulate(lambda workflow: getattr(workflow.get_last_log_entry(), 'datetime', _('None'))) }, + { + 'name': _('Completion'), + 'attribute': encapsulate(lambda workflow: getattr(workflow.get_current_state(), 'completion', _('None'))) + }, ]) register_model_list_columns(WorkflowInstanceLogEntry, [ @@ -92,6 +96,10 @@ class DocumentStatesApp(apps.AppConfig): 'name': _('Is initial state?'), 'attribute': 'initial' }, + { + 'name': _('Completion'), + 'attribute': 'completion' + }, ]) register_model_list_columns(WorkflowTransition, [ diff --git a/mayan/apps/document_states/forms.py b/mayan/apps/document_states/forms.py index f13860c15b..3be2a5e2da 100644 --- a/mayan/apps/document_states/forms.py +++ b/mayan/apps/document_states/forms.py @@ -16,7 +16,7 @@ class WorkflowForm(forms.ModelForm): class WorkflowStateForm(forms.ModelForm): class Meta: - fields = ('initial', 'label') + fields = ('initial', 'label', 'completion') model = WorkflowState diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index fd7a208d32..1921654dfc 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -5,11 +5,11 @@ from django.utils.translation import ugettext_lazy as _ from navigation import Link link_document_workflow_instance_list = Link(text=_('Workflows'), view='document_states:document_workflow_instance_list', args='object.pk') -link_setup_workflow_create = Link(text=_('Create'), view='document_states:setup_workflow_create') +link_setup_workflow_create = Link(text=_('Create workflow'), view='document_states:setup_workflow_create') link_setup_workflow_delete = Link(tags='dangerous', text=_('Delete'), view='document_states:setup_workflow_delete', args='object.pk') link_setup_workflow_document_types = Link(text=_('Document types'), view='document_states:setup_workflow_document_types', args='object.pk') link_setup_workflow_edit = Link(text=_('Edit'), view='document_states:setup_workflow_edit', args='object.pk') -link_setup_workflow_list = Link(icon='fa fa-table', text=_('Workflows'), view='document_states:setup_workflow_list') +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') diff --git a/mayan/apps/document_states/migrations/0002_workflowstate_completion.py b/mayan/apps/document_states/migrations/0002_workflowstate_completion.py new file mode 100644 index 0000000000..2adc81c633 --- /dev/null +++ b/mayan/apps/document_states/migrations/0002_workflowstate_completion.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('document_states', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='workflowstate', + name='completion', + field=models.IntegerField(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', blank=True), + preserve_default=True, + ), + ] diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index 2998639fc5..4df0701340 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -1,8 +1,10 @@ from __future__ import unicode_literals +import logging + from django.contrib.auth.models import User from django.core.urlresolvers import reverse -from django.db import models +from django.db import IntegrityError, models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ @@ -10,6 +12,8 @@ from documents.models import Document, DocumentType from .managers import WorkflowManager +logger = logging.getLogger(__name__) + @python_2_unicode_compatible class Workflow(models.Model): @@ -31,7 +35,13 @@ class Workflow(models.Model): return None def launch_for(self, document): - self.instances.create(document=document) + try: + logger.info('Launching workflow %s for document %s', self, document) + self.instances.create(document=document) + except IntegrityError: + logger.info('Workflow %s already launched for document %s', self, document) + else: + logger.info('Workflow %s launched for document %s', self, document) class Meta: verbose_name = _('Workflow') @@ -43,6 +53,7 @@ class WorkflowState(models.Model): workflow = models.ForeignKey(Workflow, related_name='states', verbose_name=_('Workflow')) label = models.CharField(max_length=255, verbose_name=_('Label')) initial = models.BooleanField(default=False, help_text=_('Select if this will be the state with which you want the workflow to start in. Only one state can be the initial state.'), verbose_name=_('Initial')) + completion = models.IntegerField(blank=True, default=0, help_text=_('Enter the percent of completion that this state represents in relation to the workflow. Use numbers without the percent sign.'), verbose_name=_('Completion')) def __str__(self): return self.label diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index ddbd9e344d..d668ab037f 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -3,14 +3,14 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url from .views import ( - SetupWorkflowCreateView, SetupWorkflowDeleteView, - SetupWorkflowDocumentTypesView, SetupWorkflowEditView, - SetupWorkflowListView, SetupWorkflowStateCreateView, - SetupWorkflowStateDeleteView, SetupWorkflowStateEditView, - SetupWorkflowStateListView, SetupWorkflowTransitionListView, - SetupWorkflowTransitionCreateView, SetupWorkflowTransitionDeleteView, - SetupWorkflowTransitionEditView, WorkflowInstanceDetailView, - WorkflowInstanceTransitionView, DocumentWorkflowInstanceListView + DocumentWorkflowInstanceListView, SetupWorkflowCreateView, + SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView, + SetupWorkflowEditView, SetupWorkflowListView, + SetupWorkflowStateCreateView, SetupWorkflowStateDeleteView, + SetupWorkflowStateEditView, SetupWorkflowStateListView, + SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView, + SetupWorkflowTransitionDeleteView, SetupWorkflowTransitionEditView, + WorkflowInstanceDetailView, WorkflowInstanceTransitionView ) urlpatterns = patterns( diff --git a/mayan/apps/document_states/views.py b/mayan/apps/document_states/views.py index 8b0ecd399c..92cefc281b 100644 --- a/mayan/apps/document_states/views.py +++ b/mayan/apps/document_states/views.py @@ -7,13 +7,13 @@ from django.db.utils import IntegrityError from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ -from django.views.generic import FormView +from django.views.generic import FormView, View from acls.models import AccessEntry from common.utils import generate_choices_w_labels from common.views import ( - AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, - SingleObjectEditView, SingleObjectListView + AssignRemoveView, ConfirmView, SingleObjectCreateView, + SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) from documents.models import Document from permissions.models import Permission @@ -60,8 +60,6 @@ class DocumentWorkflowInstanceListView(SingleObjectListView): class WorkflowInstanceDetailView(SingleObjectListView): - template_name = 'appearance/generic_multi_subtemplates.html' - def dispatch(self, request, *args, **kwargs): try: Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_WORKFLOW_VIEW]) @@ -77,33 +75,12 @@ class WorkflowInstanceDetailView(SingleObjectListView): return self.get_workflow_instance().log_entries.order_by('-datetime') def get_context_data(self, **kwargs): - form = WorkflowInstanceDetailForm( - instance=self.get_workflow_instance(), extra_fields=[ - {'label': _('Current state'), 'field': 'get_current_state'}, - {'label': _('Last transition'), 'field': 'get_last_transition'}, - ] - ) - context = { 'navigation_object_list': ['object', 'workflow_instance'], 'object': self.get_workflow_instance().document, - 'subtemplates_list': [ - { - 'name': 'appearance/generic_form_subtemplate.html', - 'context': { - 'form': form, - 'read_only': True, - } - }, - { - 'name': 'appearance/generic_list_subtemplate.html', - 'context': { - 'object_list': self.get_queryset(), - 'title': _('Log entries'), - 'hide_object': True, - } - } - ], + 'object_list': self.get_queryset(), + 'title': _('Log entries'), + 'hide_object': True, 'title': _('Detail of workflow: %(workflow)s') % { 'workflow': self.get_workflow_instance() }, @@ -193,6 +170,8 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView): def add(self, item): self.workflow.document_types.add(item) + # TODO: add task launching this workflow for all the document types of + # item def dispatch(self, request, *args, **kwargs): self.workflow = get_object_or_404(Workflow, pk=self.kwargs['pk']) @@ -212,6 +191,8 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView): def remove(self, item): self.workflow.document_types.remove(item) + # TODO: add task deleting this workflow for all the document types of + # item def get_context_data(self, **kwargs): data = super(SetupWorkflowDocumentTypesView, self).get_context_data(**kwargs) diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index f352da7a44..d05cdbb1c2 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -206,6 +206,8 @@ class APIDocumentImageView(generics.GenericAPIView): try: task = task_get_document_page_image.apply_async(kwargs=dict(document_page_id=document_page.pk, size=size, zoom=zoom, rotation=rotation, as_base64=True, version=version), queue='converter') + # TODO: prepend 'data:%s;base64,%s' based on format specified in + # async call return Response({ 'status': 'success', 'data': task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT) diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index a8dd602517..09e9934c15 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -15,6 +15,7 @@ from common import ( ) from common.api import register_maintenance_links from common.classes import ModelAttribute +from common.signals import post_initial_setup from common.utils import encapsulate, validate_path from converter.links import link_transformation_list from converter.permissions import ( @@ -29,16 +30,16 @@ from rest_api.classes import APIEndPoint from statistics.classes import StatisticNamespace from documents import settings as document_settings +from .handlers import create_default_document_type from .links import ( link_clear_image_cache, link_document_acl_list, - link_document_clear_transformations, link_document_content, - link_document_delete, link_document_document_type_edit, - link_document_events_view, link_document_multiple_document_type_edit, - link_document_download, link_document_edit, link_document_list, - link_document_list_recent, link_document_multiple_delete, + link_document_clear_transformations, link_document_delete, + link_document_document_type_edit, link_document_events_view, + link_document_multiple_document_type_edit, link_document_download, + link_document_edit, link_document_list, link_document_list_recent, + link_document_multiple_delete, link_document_multiple_clear_transformations, - link_document_multiple_download, - link_document_multiple_update_page_count, + link_document_multiple_download, link_document_multiple_update_page_count, link_document_page_navigation_first, link_document_page_navigation_last, link_document_page_navigation_next, link_document_page_navigation_previous, link_document_page_return, @@ -108,7 +109,7 @@ class DocumentsApp(apps.AppConfig): document_search.add_model_field('label', label=_('Label')) document_search.add_model_field('metadata__metadata_type__name', label=_('Metadata type')) document_search.add_model_field('metadata__value', label=_('Metadata value')) - document_search.add_model_field('versions__pages__content', label=_('Content')) + document_search.add_model_field('versions__pages__ocr_content__content', label=_('Content')) document_search.add_model_field('description', label=_('Description')) document_search.add_model_field('tags__label', label=_('Tags')) @@ -127,7 +128,6 @@ class DocumentsApp(apps.AppConfig): # Document facet links menu_facet.bind_links(links=[link_document_acl_list], sources=[Document]) menu_facet.bind_links(links=[link_document_preview], sources=[Document], position=0) - menu_facet.bind_links(links=[link_document_content], sources=[Document], position=1) menu_facet.bind_links(links=[link_document_properties], sources=[Document], position=2) menu_facet.bind_links(links=[link_document_events_view, link_document_version_list], sources=[Document], position=2) menu_facet.bind_links(links=[link_document_pages], sources=[Document]) @@ -146,6 +146,8 @@ class DocumentsApp(apps.AppConfig): namespace.add_statistic(DocumentStatistics(name='document_stats', label=_('Document tendencies'))) namespace.add_statistic(DocumentUsageStatistics(name='document_usage', label=_('Document usage'))) + post_initial_setup.connect(create_default_document_type, dispatch_uid='create_default_document_type') + registry.register(Document) register_maintenance_links([link_clear_image_cache], namespace='documents', title=_('Documents')) diff --git a/mayan/apps/documents/events.py b/mayan/apps/documents/events.py index 1e5a85a451..4f0c518022 100644 --- a/mayan/apps/documents/events.py +++ b/mayan/apps/documents/events.py @@ -7,3 +7,5 @@ from events.classes import Event event_document_create = Event(name='documents_document_create', label=_('Document created')) event_document_properties_edit = Event(name='documents_document_edit', label=_('Document properties edited')) event_document_type_change = Event(name='documents_document_type_change', label=_('Document type changed')) +event_document_new_version = Event(name='documents_document_new_version', label=_('New version uploaded')) +event_document_version_revert = Event(name='documents_document_version_revert', label=_('Document version reverted')) diff --git a/mayan/apps/documents/exceptions.py b/mayan/apps/documents/exceptions.py deleted file mode 100644 index b5c9c3eac6..0000000000 --- a/mayan/apps/documents/exceptions.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import unicode_literals - - -class NewDocumentVersionNotAllowed(Exception): - """ - Uploading new versions for this document is not allowed - Current reasons: Document is in checked out state - """ - pass diff --git a/mayan/apps/documents/forms.py b/mayan/apps/documents/forms.py index 6a99b7d73b..91d677f6bc 100644 --- a/mayan/apps/documents/forms.py +++ b/mayan/apps/documents/forms.py @@ -87,34 +87,6 @@ class DocumentPropertiesForm(DetailForm): model = Document -class DocumentContentForm(forms.Form): - """ - Form that concatenates all of a document pages' text content into a - single textarea widget - """ - def __init__(self, *args, **kwargs): - self.document = kwargs.pop('document', None) - super(DocumentContentForm, self).__init__(*args, **kwargs) - content = [] - self.fields['contents'].initial = '' - 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('\n\n\n
- %s -

\n\n\n' % (ugettext('Page %(page_number)d') % {'page_number': page.page_number})) - - self.fields['contents'].initial = mark_safe(''.join(content)) - - contents = forms.CharField( - label=_('Contents'), - widget=TextAreaDiv(attrs={'class': 'text_area_div full-height', 'data-height-difference': 360}) - ) - - class DocumentTypeSelectForm(forms.Form): """ Form to select the document type of a document to be created, used diff --git a/mayan/apps/documents/handlers.py b/mayan/apps/documents/handlers.py new file mode 100644 index 0000000000..37d33beaca --- /dev/null +++ b/mayan/apps/documents/handlers.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from .models import DocumentType + + +def create_default_document_type(sender, **kwargs): + DocumentType.objects.create(name=_('Default')) diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index 8b2e9030ca..8729039a47 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -42,7 +42,6 @@ def is_min_zoom(context): # Facet link_document_acl_list = Link(permissions=[ACLS_VIEW_ACL], text=_('ACLs'), view='documents:document_acl_list', args='object.pk') -link_document_content = Link(permissions=[PERMISSION_DOCUMENT_VIEW], text=_('Content'), view='documents:document_content', args='object.id') link_document_events_view = Link(permissions=[PERMISSION_EVENTS_VIEW], text=_('Events'), view='events:events_for_object', args=['"documents"', '"document"', 'object.id']) link_document_preview = Link(permissions=[PERMISSION_DOCUMENT_VIEW], text=_('Preview'), view='documents:document_preview', args='object.id') link_document_properties = Link(permissions=[PERMISSION_DOCUMENT_VIEW], text=_('Properties'), view='documents:document_properties', args='object.id') diff --git a/mayan/apps/documents/managers.py b/mayan/apps/documents/managers.py index 660509a463..01ba4321bd 100644 --- a/mayan/apps/documents/managers.py +++ b/mayan/apps/documents/managers.py @@ -37,6 +37,10 @@ class DocumentTypeManager(models.Manager): class DocumentManager(models.Manager): + def invalidate_cache(self): + for document in self.model.objects.all(): + document.invalidate_cache() + @transaction.atomic def new_document(self, document_type, file_object, label=None, command_line=False, description=None, expand=False, language=None, user=None): versions_created = [] diff --git a/mayan/apps/documents/migrations/0004_auto_20150616_1930.py b/mayan/apps/documents/migrations/0004_auto_20150616_1930.py new file mode 100644 index 0000000000..879a9b1ca7 --- /dev/null +++ b/mayan/apps/documents/migrations/0004_auto_20150616_1930.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0003_auto_20150608_1915'), + ] + + operations = [ + migrations.AlterModelOptions( + name='document', + options={'ordering': ('-date_added',), 'verbose_name': 'Document', 'verbose_name_plural': 'Documents'}, + ), + migrations.AlterModelOptions( + name='documentpage', + options={'ordering': ('page_number',), 'verbose_name': 'Document page', 'verbose_name_plural': 'Document pages'}, + ), + migrations.AlterModelOptions( + name='documenttype', + options={'ordering': ('name',), 'verbose_name': 'Document type', 'verbose_name_plural': 'Documents types'}, + ), + migrations.AlterModelOptions( + name='documenttypefilename', + options={'ordering': ('filename',), 'verbose_name': 'Document type quick rename filename', 'verbose_name_plural': 'Document types quick rename filenames'}, + ), + ] diff --git a/mayan/apps/documents/migrations/0005_auto_20150617_0358.py b/mayan/apps/documents/migrations/0005_auto_20150617_0358.py new file mode 100644 index 0000000000..93a635f480 --- /dev/null +++ b/mayan/apps/documents/migrations/0005_auto_20150617_0358.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0004_auto_20150616_1930'), + ] + + operations = [ + migrations.RenameField( + model_name='documentpage', + old_name='content', + new_name='content_old', + ), + ] diff --git a/mayan/apps/documents/migrations/0006_remove_documentpage_content_old.py b/mayan/apps/documents/migrations/0006_remove_documentpage_content_old.py new file mode 100644 index 0000000000..ad0ddd3b83 --- /dev/null +++ b/mayan/apps/documents/migrations/0006_remove_documentpage_content_old.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0005_auto_20150617_0358'), + ('ocr', '0003_auto_20150617_0401'), + ] + + operations = [ + migrations.RemoveField( + model_name='documentpage', + name='content_old', + ), + ] diff --git a/mayan/apps/documents/migrations/0007_remove_documentpage_page_label.py b/mayan/apps/documents/migrations/0007_remove_documentpage_page_label.py new file mode 100644 index 0000000000..b116ca8c12 --- /dev/null +++ b/mayan/apps/documents/migrations/0007_remove_documentpage_page_label.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0006_remove_documentpage_content_old'), + ] + + operations = [ + migrations.RemoveField( + model_name='documentpage', + name='page_label', + ), + ] diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py index 94041b4cdd..4042d7b158 100644 --- a/mayan/apps/documents/models.py +++ b/mayan/apps/documents/models.py @@ -28,8 +28,10 @@ from converter.literals import ( from converter.models import Transformation from mimetype.api import get_mimetype -from .events import event_document_create -from .exceptions import NewDocumentVersionNotAllowed +from .events import ( + event_document_create, event_document_new_version, + event_document_version_revert +) from .managers import ( DocumentManager, DocumentTypeManager, RecentDocumentManager ) @@ -70,7 +72,7 @@ class DocumentType(models.Model): class Meta: verbose_name = _('Document type') verbose_name_plural = _('Documents types') - ordering = ['name'] + ordering = ('name',) @python_2_unicode_compatible @@ -91,7 +93,7 @@ class Document(models.Model): class Meta: verbose_name = _('Document') verbose_name_plural = _('Documents') - ordering = ['-date_added'] + ordering = ('-date_added',) def set_document_type(self, document_type, force=False): has_changed = self.document_type != document_type @@ -101,12 +103,9 @@ class Document(models.Model): if has_changed or force: post_document_type_change.send(sender=self.__class__, instance=self) - @staticmethod - def clear_image_cache(): - for the_file in os.listdir(CACHE_PATH): - file_path = os.path.join(CACHE_PATH, the_file) - if os.path.isfile(file_path): - os.unlink(file_path) + def invalidate_cache(self): + for document_version in self.versions.all(): + document_version.invalidate_cache() def __str__(self): return self.label @@ -128,13 +127,6 @@ class Document(models.Model): else: event_document_create.commit(target=self) - def invalidate_cached_image(self, page): - pass - #try: - # os.unlink(self.get_cached_image_name(page, self.latest_version.pk)[0]) - #except OSError: - # pass - def add_as_recent_document_for_user(self, user): RecentDocument.objects.add_document_for_user(user, self) @@ -148,10 +140,7 @@ class Document(models.Model): return self.latest_version.size def new_version(self, file_object, user=None, comment=None): - logger.debug('creating new document version') - # TODO: move this restriction to a signal processor of the checkouts app - if not self.is_new_versions_allowed(user=user): - raise NewDocumentVersionNotAllowed + logger.info('Creating a new document version for document: %s', self) new_version = DocumentVersion.objects.create( document=self, @@ -159,9 +148,9 @@ class Document(models.Model): comment=comment or '', ) - logger.debug('new_version saved') + logger.info('New document version created for document: %s', self) - # TODO: new HISTORY for version updates + event_document_new_version.commit(actor=user, target=self) return new_version @@ -286,6 +275,10 @@ class DocumentVersion(models.Model): post_version_upload.send(sender=self.__class__, instance=self) + def invalidate_cache(self): + for page in self.pages.all(): + page.invalidate_cache() + def update_checksum(self, save=True): """ Open a document version's file and update the checksum field using the @@ -322,10 +315,14 @@ class DocumentVersion(models.Model): return detected_pages - def revert(self): + def revert(self, user=None): """ Delete the subsequent versions after this one """ + logger.info('Reverting to document document: %s to version: %s', self.document, self) + + event_document_version_revert.commit(actor=user, target=self.document) + for version in self.document.versions.filter(timestamp__gt=self.timestamp): version.delete() @@ -346,7 +343,11 @@ class DocumentVersion(models.Model): self.save() def delete(self, *args, **kwargs): + for page in self.pages.all(): + page.delete() + self.file.storage.delete(self.file.path) + return super(DocumentVersion, self).delete(*args, **kwargs) def exists(self): @@ -414,7 +415,7 @@ class DocumentTypeFilename(models.Model): return self.filename class Meta: - ordering = ['filename'] + ordering = ('filename',) unique_together = ('document_type', 'filename') verbose_name = _('Document type quick rename filename') verbose_name_plural = _('Document types quick rename filenames') @@ -423,11 +424,9 @@ class DocumentTypeFilename(models.Model): @python_2_unicode_compatible class DocumentPage(models.Model): """ - Model that describes a document version page including it's content + Model that describes a document version page """ document_version = models.ForeignKey(DocumentVersion, verbose_name=_('Document version'), related_name='pages') - content = models.TextField(blank=True, null=True, verbose_name=_('Content')) - page_label = models.CharField(max_length=40, blank=True, null=True, verbose_name=_('Page label')) page_number = models.PositiveIntegerField(default=1, editable=False, verbose_name=_('Page number'), db_index=True) def __str__(self): @@ -438,13 +437,17 @@ class DocumentPage(models.Model): } class Meta: - ordering = ['page_number'] + ordering = ('page_number',) verbose_name = _('Document page') verbose_name_plural = _('Document pages') def get_absolute_url(self): return reverse('documents:document_page_view', args=[self.pk]) + def delete(self, *args, **kwargs): + self.invalidate_cache() + super(DocumentPage, self).delete(*args, **kwargs) + @property def siblings(self): return DocumentPage.objects.filter(document_version=self.document_version) @@ -454,15 +457,20 @@ class DocumentPage(models.Model): def document(self): return self.document_version.document + def invalidate_cache(self): + fs_cleanup(self.get_cache_filename()) + def get_uuid(self): - return 'page-cache-{}'.format(self.pk) + # Make cache UUID a mix of document UUID, version ID and page ID to + # avoid using stale images + return 'page-cache-{}-{}-{}'.format(self.document.uuid, self.document_version.pk, self.pk) def get_cache_filename(self): return os.path.join(CACHE_PATH, self.get_uuid()) def get_image(self, *args, **kwargs): + as_base64 = kwargs.pop('as_base64', False) transformations = kwargs.pop('transformations', []) - size = kwargs.pop('size', DISPLAY_SIZE) rotation = kwargs.pop('rotation', DEFAULT_ROTATION) zoom_level = kwargs.pop('zoom', DEFAULT_ZOOM_LEVEL) @@ -475,8 +483,6 @@ class DocumentPage(models.Model): rotation = rotation % 360 - as_base64 = kwargs.pop('as_base64', False) - cache_filename = self.get_cache_filename() if os.path.exists(cache_filename): @@ -492,6 +498,7 @@ class DocumentPage(models.Model): with open(cache_filename, 'wb+') as file_object: file_object.write(page_image.getvalue()) except: + # Cleanup in case of error fs_cleanup(cache_filename) raise @@ -515,6 +522,7 @@ class DocumentPage(models.Model): page_image = converter.get_page() if as_base64: + # TODO: don't prepend 'data:%s;base64,%s' part return 'data:%s;base64,%s' % ('image/png', base64.b64encode(page_image.getvalue())) else: return page_image diff --git a/mayan/apps/documents/tasks.py b/mayan/apps/documents/tasks.py index 26a06a23c3..6f107e761d 100644 --- a/mayan/apps/documents/tasks.py +++ b/mayan/apps/documents/tasks.py @@ -22,8 +22,10 @@ def task_get_document_page_image(document_page_id, *args, **kwargs): @app.task(ignore_result=True) def task_clear_image_cache(): - # TODO: Error logging / notification - Document.clear_image_cache() + logger.info('Starting document cache invalidation') + # TODO: Notification of success and of errors + Document.objects.invalidate_cache() + logger.info('Finished document cache invalidation') @app.task(ignore_result=True) @@ -69,6 +71,9 @@ def task_upload_new_version(document_id, shared_uploaded_file_id, user_id, comme user = None with File(file=shared_file.file) as file_object: - document.new_version(comment=comment, file_object=file_object, user=user) - - shared_file.delete() + try: + document.new_version(comment=comment, file_object=file_object, user=user) + except Warning as warning: + logger.info('Warning during attempt to create new document version for document:%s ; %s', document, warning) + finally: + shared_file.delete() diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index 1aa185d5bb..f91b787f92 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -19,58 +19,57 @@ urlpatterns = patterns( url(r'^list/$', DocumentListView.as_view(), name='document_list'), url(r'^list/recent/$', RecentDocumentListView.as_view(), name='document_list_recent'), - url(r'^(?P\d+)/preview/$', 'document_preview', (), 'document_preview'), - url(r'^(?P\d+)/content/$', 'document_content', (), 'document_content'), - url(r'^(?P\d+)/properties/$', 'document_properties', (), 'document_properties'), + url(r'^(?P\d+)/preview/$', 'document_preview', name='document_preview'), + url(r'^(?P\d+)/properties/$', 'document_properties', name='document_properties'), url(r'^(?P\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\d+)/delete/$', 'document_delete', (), 'document_delete'), - url(r'^multiple/delete/$', 'document_multiple_delete', (), 'document_multiple_delete'), - url(r'^(?P\d+)/edit/$', 'document_edit', (), 'document_edit'), - url(r'^(?P\d+)/print/$', 'document_print', (), 'document_print'), - url(r'^(?P\d+)/reset_page_count/$', 'document_update_page_count', (), 'document_update_page_count'), - url(r'^multiple/reset_page_count/$', 'document_multiple_update_page_count', (), 'document_multiple_update_page_count'), + url(r'^(?P\d+)/delete/$', 'document_delete', name='document_delete'), + url(r'^multiple/delete/$', 'document_multiple_delete', name='document_multiple_delete'), + url(r'^(?P\d+)/edit/$', 'document_edit', name='document_edit'), + url(r'^(?P\d+)/print/$', 'document_print', name='document_print'), + url(r'^(?P\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\d+)/acls/$', 'document_acl_list', (), 'document_acl_list'), + url(r'^(?P\d+)/acls/$', 'document_acl_list', name='document_acl_list'), url(r'^(?P\d+)/display/$', 'get_document_image', {'size': DISPLAY_SIZE}, 'document_display'), url(r'^(?P\d+)/display/print/$', 'get_document_image', {'size': PRINT_SIZE}, 'document_display_print'), - url(r'^(?P\d+)/download/$', 'document_download', (), 'document_download'), - url(r'^multiple/download/$', 'document_multiple_download', (), 'document_multiple_download'), - url(r'^(?P\d+)/clear_transformations/$', 'document_clear_transformations', (), 'document_clear_transformations'), + url(r'^(?P\d+)/download/$', 'document_download', name='document_download'), + url(r'^multiple/download/$', 'document_multiple_download', name='document_multiple_download'), + url(r'^(?P\d+)/clear_transformations/$', 'document_clear_transformations', name='document_clear_transformations'), - url(r'^(?P\d+)/version/all/$', 'document_version_list', (), 'document_version_list'), - url(r'^document/version/(?P\d+)/download/$', 'document_download', (), 'document_version_download'), - url(r'^document/version/(?P\d+)/revert/$', 'document_version_revert', (), 'document_version_revert'), + url(r'^(?P\d+)/version/all/$', 'document_version_list', name='document_version_list'), + url(r'^document/version/(?P\d+)/download/$', 'document_download', name='document_version_download'), + url(r'^document/version/(?P\d+)/revert/$', 'document_version_revert', name='document_version_revert'), - url(r'^(?P\d+)/acls/$', 'document_acl_list', (), 'document_acl_list'), + url(r'^(?P\d+)/acls/$', 'document_acl_list', name='document_acl_list'), url(r'^(?P\d+)/pages/all/$', DocumentPageListView.as_view(), name='document_pages'), - url(r'^multiple/clear_transformations/$', 'document_multiple_clear_transformations', (), 'document_multiple_clear_transformations'), - url(r'^maintenance/clear_image_cache/$', 'document_clear_image_cache', (), 'document_clear_image_cache'), + url(r'^multiple/clear_transformations/$', 'document_multiple_clear_transformations', name='document_multiple_clear_transformations'), + url(r'^maintenance/clear_image_cache/$', 'document_clear_image_cache', name='document_clear_image_cache'), - url(r'^page/(?P\d+)/$', 'document_page_view', (), 'document_page_view'), - url(r'^page/(?P\d+)/navigation/next/$', 'document_page_navigation_next', (), 'document_page_navigation_next'), - url(r'^page/(?P\d+)/navigation/previous/$', 'document_page_navigation_previous', (), 'document_page_navigation_previous'), - url(r'^page/(?P\d+)/navigation/first/$', 'document_page_navigation_first', (), 'document_page_navigation_first'), - url(r'^page/(?P\d+)/navigation/last/$', 'document_page_navigation_last', (), 'document_page_navigation_last'), - url(r'^page/(?P\d+)/zoom/in/$', 'document_page_zoom_in', (), 'document_page_zoom_in'), - url(r'^page/(?P\d+)/zoom/out/$', 'document_page_zoom_out', (), 'document_page_zoom_out'), - url(r'^page/(?P\d+)/rotate/right/$', 'document_page_rotate_right', (), 'document_page_rotate_right'), - url(r'^page/(?P\d+)/rotate/left/$', 'document_page_rotate_left', (), 'document_page_rotate_left'), - url(r'^page/(?P\d+)/reset/$', 'document_page_view_reset', (), 'document_page_view_reset'), + url(r'^page/(?P\d+)/$', 'document_page_view', name='document_page_view'), + url(r'^page/(?P\d+)/navigation/next/$', 'document_page_navigation_next', name='document_page_navigation_next'), + url(r'^page/(?P\d+)/navigation/previous/$', 'document_page_navigation_previous', name='document_page_navigation_previous'), + url(r'^page/(?P\d+)/navigation/first/$', 'document_page_navigation_first', name='document_page_navigation_first'), + url(r'^page/(?P\d+)/navigation/last/$', 'document_page_navigation_last', name='document_page_navigation_last'), + url(r'^page/(?P\d+)/zoom/in/$', 'document_page_zoom_in', name='document_page_zoom_in'), + url(r'^page/(?P\d+)/zoom/out/$', 'document_page_zoom_out', name='document_page_zoom_out'), + url(r'^page/(?P\d+)/rotate/right/$', 'document_page_rotate_right', name='document_page_rotate_right'), + url(r'^page/(?P\d+)/rotate/left/$', 'document_page_rotate_left', name='document_page_rotate_left'), + url(r'^page/(?P\d+)/reset/$', 'document_page_view_reset', name='document_page_view_reset'), # Admin views - url(r'^type/list/$', 'document_type_list', (), 'document_type_list'), - url(r'^type/create/$', 'document_type_create', (), 'document_type_create'), - url(r'^type/(?P\d+)/edit/$', 'document_type_edit', (), 'document_type_edit'), - url(r'^type/(?P\d+)/delete/$', 'document_type_delete', (), 'document_type_delete'), + url(r'^type/list/$', 'document_type_list', name='document_type_list'), + url(r'^type/create/$', 'document_type_create', name='document_type_create'), + url(r'^type/(?P\d+)/edit/$', 'document_type_edit', name='document_type_edit'), + url(r'^type/(?P\d+)/delete/$', 'document_type_delete', name='document_type_delete'), - url(r'^type/(?P\d+)/filename/list/$', 'document_type_filename_list', (), 'document_type_filename_list'), - url(r'^type/filename/(?P\d+)/edit/$', 'document_type_filename_edit', (), 'document_type_filename_edit'), - url(r'^type/filename/(?P\d+)/delete/$', 'document_type_filename_delete', (), 'document_type_filename_delete'), - url(r'^type/(?P\d+)/filename/create/$', 'document_type_filename_create', (), 'document_type_filename_create'), + url(r'^type/(?P\d+)/filename/list/$', 'document_type_filename_list', name='document_type_filename_list'), + url(r'^type/filename/(?P\d+)/edit/$', 'document_type_filename_edit', name='document_type_filename_edit'), + url(r'^type/filename/(?P\d+)/delete/$', 'document_type_filename_delete', name='document_type_filename_delete'), + url(r'^type/(?P\d+)/filename/create/$', 'document_type_filename_create', name='document_type_filename_create'), ) api_urls = patterns( diff --git a/mayan/apps/documents/views.py b/mayan/apps/documents/views.py index 4532815134..afdf529ee8 100644 --- a/mayan/apps/documents/views.py +++ b/mayan/apps/documents/views.py @@ -33,10 +33,9 @@ from .events import ( event_document_properties_edit, event_document_type_change ) from .forms import ( - DocumentContentForm, DocumentDownloadForm, DocumentForm, DocumentPageForm, - DocumentPreviewForm, DocumentPropertiesForm, DocumentTypeForm, - DocumentTypeFilenameForm, DocumentTypeFilenameForm_create, - DocumentTypeSelectForm, PrintForm + DocumentDownloadForm, DocumentForm, DocumentPageForm, DocumentPreviewForm, + DocumentPropertiesForm, DocumentTypeForm, DocumentTypeFilenameForm, + DocumentTypeFilenameForm_create, DocumentTypeSelectForm, PrintForm ) from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT from .models import ( @@ -188,28 +187,6 @@ def document_preview(request, document_id): }, context_instance=RequestContext(request)) -def document_content(request, document_id): - document = get_object_or_404(Document, pk=document_id) - - try: - Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_VIEW]) - except PermissionDenied: - AccessEntry.objects.check_access(PERMISSION_DOCUMENT_VIEW, request.user, document) - - document.add_as_recent_document_for_user(request.user) - - content_form = DocumentContentForm(document=document) - - return render_to_response('appearance/generic_form.html', { - 'document': document, - 'form': content_form, - 'hide_labels': True, - 'object': document, - 'read_only': True, - 'title': _('Content of document: %s') % document, - }, context_instance=RequestContext(request)) - - def document_delete(request, document_id=None, document_id_list=None): post_action_redirect = None @@ -1095,7 +1072,7 @@ def document_version_revert(request, document_version_pk): if request.method == 'POST': try: - document_version.revert() + document_version.revert(user=request.user) messages.success(request, _('Document version reverted successfully')) except Exception as exception: messages.error(request, _('Error reverting document version; %s') % exception) diff --git a/mayan/apps/dynamic_search/urls.py b/mayan/apps/dynamic_search/urls.py index 20ec32cb06..d0a7365294 100644 --- a/mayan/apps/dynamic_search/urls.py +++ b/mayan/apps/dynamic_search/urls.py @@ -8,10 +8,10 @@ from .api_views import ( urlpatterns = patterns( 'dynamic_search.views', - url(r'^$', 'search', (), 'search'), + url(r'^$', 'search', name='search'), url(r'^advanced/$', 'search', {'advanced': True}, 'search_advanced'), - url(r'^again/$', 'search_again', (), 'search_again'), - url(r'^results/$', 'results', (), 'results'), + url(r'^again/$', 'search_again', name='search_again'), + url(r'^results/$', 'results', name='results'), ) api_urls = patterns( diff --git a/mayan/apps/events/urls.py b/mayan/apps/events/urls.py index 0d0a13e2a2..820e84743b 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -4,7 +4,7 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'events.views', - url(r'^all/$', 'events_list', (), 'events_list'), - url(r'^for_object/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', 'events_list', (), 'events_for_object'), - url(r'^by_verb/(?P[\w\-]+)/$', 'events_list', (), 'events_by_verb'), + url(r'^all/$', 'events_list', name='events_list'), + url(r'^for_object/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', 'events_list', name='events_for_object'), + url(r'^by_verb/(?P[\w\-]+)/$', 'events_list', name='events_by_verb'), ) diff --git a/mayan/apps/folders/urls.py b/mayan/apps/folders/urls.py index 2aacaf5ead..ecca4caf2f 100644 --- a/mayan/apps/folders/urls.py +++ b/mayan/apps/folders/urls.py @@ -11,17 +11,17 @@ from .views import FolderDetailView, FolderListView urlpatterns = patterns( 'folders.views', url(r'^list/$', FolderListView.as_view(), name='folder_list'), - url(r'^create/$', 'folder_create', (), 'folder_create'), - url(r'^(?P\d+)/edit/$', 'folder_edit', (), 'folder_edit'), - url(r'^(?P\d+)/delete/$', 'folder_delete', (), 'folder_delete'), + url(r'^create/$', 'folder_create', name='folder_create'), + url(r'^(?P\d+)/edit/$', 'folder_edit', name='folder_edit'), + url(r'^(?P\d+)/delete/$', 'folder_delete', name='folder_delete'), url(r'^(?P\d+)/$', FolderDetailView.as_view(), name='folder_view'), - url(r'^(?P\d+)/remove/document/multiple/$', 'folder_document_multiple_remove', (), 'folder_document_multiple_remove'), + url(r'^(?P\d+)/remove/document/multiple/$', 'folder_document_multiple_remove', name='folder_document_multiple_remove'), - url(r'^document/(?P\d+)/folder/add/$', 'folder_add_document', (), 'folder_add_document'), - url(r'^document/multiple/folder/add/$', 'folder_add_multiple_documents', (), 'folder_add_multiple_documents'), - url(r'^document/(?P\d+)/folder/list/$', 'document_folder_list', (), 'document_folder_list'), + url(r'^document/(?P\d+)/folder/add/$', 'folder_add_document', name='folder_add_document'), + url(r'^document/multiple/folder/add/$', 'folder_add_multiple_documents', name='folder_add_multiple_documents'), + url(r'^document/(?P\d+)/folder/list/$', 'document_folder_list', name='document_folder_list'), - url(r'^(?P\d+)/acl/list/$', 'folder_acl_list', (), 'folder_acl_list'), + url(r'^(?P\d+)/acl/list/$', 'folder_acl_list', name='folder_acl_list'), ) api_urls = patterns( diff --git a/mayan/apps/installation/links.py b/mayan/apps/installation/links.py index 5214ac505c..f81fa01746 100644 --- a/mayan/apps/installation/links.py +++ b/mayan/apps/installation/links.py @@ -6,6 +6,6 @@ from navigation import Link from .permissions import PERMISSION_INSTALLATION_DETAILS -link_menu_link = Link(icon='fa fa-flag', permissions=[PERMISSION_INSTALLATION_DETAILS], text=_('Installation details'), view='installation:namespace_list') +link_menu_link = Link(icon='fa fa-check-square-o', permissions=[PERMISSION_INSTALLATION_DETAILS], text=_('Installation details'), view='installation:namespace_list') link_namespace_details = Link(permissions=[PERMISSION_INSTALLATION_DETAILS], text=_('Details'), view='installation:namespace_details', args='object.id') link_namespace_list = Link(permissions=[PERMISSION_INSTALLATION_DETAILS], text=_('Installation property namespaces'), view='installation:namespace_list') diff --git a/mayan/apps/installation/urls.py b/mayan/apps/installation/urls.py index 42b8faa471..a734970a5c 100644 --- a/mayan/apps/installation/urls.py +++ b/mayan/apps/installation/urls.py @@ -4,6 +4,6 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'installation.views', - url(r'^$', 'namespace_list', (), 'namespace_list'), - url(r'^(?P\w+)/details/$', 'namespace_details', (), 'namespace_details'), + url(r'^$', 'namespace_list', name='namespace_list'), + url(r'^(?P\w+)/details/$', 'namespace_details', name='namespace_details'), ) diff --git a/mayan/apps/linking/urls.py b/mayan/apps/linking/urls.py index 12f5c1d1b7..3281c80124 100644 --- a/mayan/apps/linking/urls.py +++ b/mayan/apps/linking/urls.py @@ -6,19 +6,19 @@ from .views import SetupSmartLinkDocumentTypesView urlpatterns = patterns( 'linking.views', - url(r'^document/(?P\d+)/$', 'smart_link_instances_for_document', (), 'smart_link_instances_for_document'), - url(r'^document/(?P\d+)/smart_link/(?P\d+)/$', 'smart_link_instance_view', (), 'smart_link_instance_view'), + url(r'^document/(?P\d+)/$', 'smart_link_instances_for_document', name='smart_link_instances_for_document'), + url(r'^document/(?P\d+)/smart_link/(?P\d+)/$', 'smart_link_instance_view', name='smart_link_instance_view'), - url(r'^setup/list/$', 'smart_link_list', (), 'smart_link_list'), - url(r'^setup/create/$', 'smart_link_create', (), 'smart_link_create'), - url(r'^setup/(?P\d+)/delete/$', 'smart_link_delete', (), 'smart_link_delete'), - url(r'^setup/(?P\d+)/edit/$', 'smart_link_edit', (), 'smart_link_edit'), + url(r'^setup/list/$', 'smart_link_list', name='smart_link_list'), + url(r'^setup/create/$', 'smart_link_create', name='smart_link_create'), + url(r'^setup/(?P\d+)/delete/$', 'smart_link_delete', name='smart_link_delete'), + url(r'^setup/(?P\d+)/edit/$', 'smart_link_edit', name='smart_link_edit'), url(r'^setup/(?P\d+)/document_types/$', SetupSmartLinkDocumentTypesView.as_view(), name='smart_link_document_types'), - url(r'^setup/(?P\d+)/condition/list/$', 'smart_link_condition_list', (), 'smart_link_condition_list'), - url(r'^setup/(?P\d+)/condition/create/$', 'smart_link_condition_create', (), 'smart_link_condition_create'), - url(r'^setup/smart_link/condition/(?P\d+)/edit/$', 'smart_link_condition_edit', (), 'smart_link_condition_edit'), - url(r'^setup/smart_link/condition/(?P\d+)/delete/$', 'smart_link_condition_delete', (), 'smart_link_condition_delete'), + url(r'^setup/(?P\d+)/condition/list/$', 'smart_link_condition_list', name='smart_link_condition_list'), + url(r'^setup/(?P\d+)/condition/create/$', 'smart_link_condition_create', name='smart_link_condition_create'), + url(r'^setup/smart_link/condition/(?P\d+)/edit/$', 'smart_link_condition_edit', name='smart_link_condition_edit'), + url(r'^setup/smart_link/condition/(?P\d+)/delete/$', 'smart_link_condition_delete', name='smart_link_condition_delete'), - url(r'^(?P\d+)/acl/list/$', 'smart_link_acl_list', (), 'smart_link_acl_list'), + url(r'^(?P\d+)/acl/list/$', 'smart_link_acl_list', name='smart_link_acl_list'), ) diff --git a/mayan/apps/mailer/urls.py b/mayan/apps/mailer/urls.py index 58beb532ed..3de1fa9b77 100644 --- a/mayan/apps/mailer/urls.py +++ b/mayan/apps/mailer/urls.py @@ -4,6 +4,6 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'mailer.views', - url(r'^(?P\d+)/send/link/$', 'send_document_link', (), 'send_document_link'), + url(r'^(?P\d+)/send/link/$', 'send_document_link', name='send_document_link'), url(r'^(?P\d+)/send/document/$', 'send_document_link', {'as_attachment': True}, 'send_document'), ) diff --git a/mayan/apps/metadata/links.py b/mayan/apps/metadata/links.py index 3fde58cdf3..c2b9ab1231 100644 --- a/mayan/apps/metadata/links.py +++ b/mayan/apps/metadata/links.py @@ -25,4 +25,4 @@ link_setup_document_type_metadata_required = Link(permissions=[PERMISSION_DOCUME link_setup_metadata_type_create = Link(permissions=[PERMISSION_METADATA_TYPE_CREATE], text=_('Create new'), view='metadata:setup_metadata_type_create') link_setup_metadata_type_delete = Link(permissions=[PERMISSION_METADATA_TYPE_DELETE], tags='dangerous', text=_('Delete'), view='metadata:setup_metadata_type_delete', args='object.pk') link_setup_metadata_type_edit = Link(permissions=[PERMISSION_METADATA_TYPE_EDIT], text=_('Edit'), view='metadata:setup_metadata_type_edit', args='object.pk') -link_setup_metadata_type_list = Link(icon='fa fa-list', permissions=[PERMISSION_METADATA_TYPE_VIEW], text=_('Metadata types'), view='metadata:setup_metadata_type_list') +link_setup_metadata_type_list = Link(icon='fa fa-pencil', permissions=[PERMISSION_METADATA_TYPE_VIEW], text=_('Metadata types'), view='metadata:setup_metadata_type_list') diff --git a/mayan/apps/metadata/urls.py b/mayan/apps/metadata/urls.py index c899c4e598..2efd3228c9 100644 --- a/mayan/apps/metadata/urls.py +++ b/mayan/apps/metadata/urls.py @@ -17,18 +17,18 @@ from .views import ( urlpatterns = patterns( 'metadata.views', - url(r'^(?P\d+)/edit/$', 'metadata_edit', (), 'metadata_edit'), - url(r'^(?P\d+)/view/$', 'metadata_view', (), 'metadata_view'), - url(r'^multiple/edit/$', 'metadata_multiple_edit', (), 'metadata_multiple_edit'), - url(r'^(?P\d+)/add/$', 'metadata_add', (), 'metadata_add'), - url(r'^multiple/add/$', 'metadata_multiple_add', (), 'metadata_multiple_add'), - url(r'^(?P\d+)/remove/$', 'metadata_remove', (), 'metadata_remove'), - url(r'^multiple/remove/$', 'metadata_multiple_remove', (), 'metadata_multiple_remove'), + url(r'^(?P\d+)/edit/$', 'metadata_edit', name='metadata_edit'), + url(r'^(?P\d+)/view/$', 'metadata_view', name='metadata_view'), + url(r'^multiple/edit/$', 'metadata_multiple_edit', name='metadata_multiple_edit'), + url(r'^(?P\d+)/add/$', 'metadata_add', name='metadata_add'), + url(r'^multiple/add/$', 'metadata_multiple_add', name='metadata_multiple_add'), + url(r'^(?P\d+)/remove/$', 'metadata_remove', name='metadata_remove'), + url(r'^multiple/remove/$', 'metadata_multiple_remove', name='metadata_multiple_remove'), - url(r'^setup/type/list/$', 'setup_metadata_type_list', (), 'setup_metadata_type_list'), - url(r'^setup/type/create/$', 'setup_metadata_type_create', (), 'setup_metadata_type_create'), - url(r'^setup/type/(?P\d+)/edit/$', 'setup_metadata_type_edit', (), 'setup_metadata_type_edit'), - url(r'^setup/type/(?P\d+)/delete/$', 'setup_metadata_type_delete', (), 'setup_metadata_type_delete'), + url(r'^setup/type/list/$', 'setup_metadata_type_list', name='setup_metadata_type_list'), + url(r'^setup/type/create/$', 'setup_metadata_type_create', name='setup_metadata_type_create'), + url(r'^setup/type/(?P\d+)/edit/$', 'setup_metadata_type_edit', name='setup_metadata_type_edit'), + url(r'^setup/type/(?P\d+)/delete/$', 'setup_metadata_type_delete', name='setup_metadata_type_delete'), url(r'^setup/document/type/(?P\d+)/metadata/edit/$', SetupDocumentTypeMetadataOptionalView.as_view(), name='setup_document_type_metadata'), url(r'^setup/document/type/(?P\d+)/metadata/edit/required/$', SetupDocumentTypeMetadataRequiredView.as_view(), name='setup_document_type_metadata_required'), diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index 86a6c4deb5..d0f6a502e5 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -86,29 +86,43 @@ class Menu(object): pass for resolved_navigation_object in resolved_navigation_object_list: - for source, links in self.bound_links.iteritems(): + resolved_links = [] + + for bound_source, links in self.bound_links.iteritems(): try: - if inspect.isclass(source) and isinstance(resolved_navigation_object, source) or source == CombinedSource(obj=resolved_navigation_object.__class__, view=current_view): + if inspect.isclass(bound_source) and isinstance(resolved_navigation_object, bound_source) or source == CombinedSource(obj=resolved_navigation_object.__class__, view=current_view): for link in links: resolved_link = link.resolve(context=context, resolved_object=resolved_navigation_object) if resolved_link: - result.append(resolved_link) + resolved_links.append(resolved_link) break # No need for further content object match testing except TypeError: # When source is a dictionary pass + if resolved_links: + result.append(resolved_links) + + resolved_links = [] # View links for link in self.bound_links.get(current_view, []): resolved_link = link.resolve(context) if resolved_link: - result.append(resolved_link) + resolved_links.append(resolved_link) + + if resolved_links: + result.append(resolved_links) + + resolved_links = [] # Main menu links for link in self.bound_links.get(None, []): resolved_link = link.resolve(context) if resolved_link: - result.append(resolved_link) + resolved_links.append(resolved_link) + + if resolved_links: + result.append(resolved_links) return result diff --git a/mayan/apps/navigation/templatetags/navigation_tags.py b/mayan/apps/navigation/templatetags/navigation_tags.py index e88070b67f..391fe399bb 100644 --- a/mayan/apps/navigation/templatetags/navigation_tags.py +++ b/mayan/apps/navigation/templatetags/navigation_tags.py @@ -20,16 +20,20 @@ def get_menus_links(context, names, source=None): result = [] for name in names.split(','): - links = Menu.get(name=name).resolve(context) - if links: - result.append(links) + for links in Menu.get(name=name).resolve(context): + if links: + result.append(links) return result @register.simple_tag(takes_context=True) def get_multi_item_links_form(context, object_list): - actions = [(link.url, link.text) for link in Menu.get('multi item menu').resolve(context=context, source=object_list[0])] + actions = [] + for link_set in Menu.get('multi item menu').resolve(context=context, source=object_list[0]): + for link in link_set: + actions.append((link.url, link.text)) + form = MultiItemForm(actions=actions) context.update({'multi_item_form': form, 'multi_item_actions': actions}) return '' diff --git a/mayan/apps/ocr/apps.py b/mayan/apps/ocr/apps.py index 6b5d5daa69..d93490afd2 100644 --- a/mayan/apps/ocr/apps.py +++ b/mayan/apps/ocr/apps.py @@ -8,7 +8,9 @@ from django import apps from django.utils.translation import ugettext_lazy as _ from acls.api import class_permissions -from common import menu_multi_item, menu_object, menu_secondary, menu_tools +from common import ( + menu_facet, menu_multi_item, menu_object, menu_secondary, menu_tools +) from common.api import register_maintenance_links from common.utils import encapsulate from documents.models import Document, DocumentVersion @@ -20,12 +22,13 @@ from rest_api.classes import APIEndPoint from .handlers import post_version_upload_ocr from .links import ( - link_document_submit, link_document_submit_multiple, link_entry_delete, + link_document_content, link_document_submit, + link_document_submit_multiple, link_entry_delete, link_entry_delete_multiple, link_entry_list, link_entry_re_queue, link_entry_re_queue_multiple ) from .models import DocumentVersionOCRError -from .permissions import PERMISSION_OCR_DOCUMENT +from .permissions import PERMISSION_OCR_DOCUMENT, PERMISSION_OCR_CONTENT_VIEW from .settings import PDFTOTEXT_PATH, TESSERACT_PATH, UNPAPER_PATH from .tasks import task_do_ocr @@ -50,8 +53,13 @@ class OCRApp(apps.AppConfig): Document.add_to_class('submit_for_ocr', document_ocr_submit) DocumentVersion.add_to_class('submit_for_ocr', document_version_ocr_submit) - class_permissions(Document, [PERMISSION_OCR_DOCUMENT]) + class_permissions( + Document, [ + PERMISSION_OCR_DOCUMENT, PERMISSION_OCR_CONTENT_VIEW + ] + ) + menu_facet.bind_links(links=[link_document_content], sources=[Document]) menu_multi_item.bind_links(links=[link_document_submit_multiple], sources=[Document]) menu_multi_item.bind_links(links=[link_entry_re_queue_multiple, link_entry_delete_multiple], sources=[DocumentVersionOCRError]) menu_object.bind_links(links=[link_document_submit], sources=[Document]) diff --git a/mayan/apps/ocr/classes.py b/mayan/apps/ocr/classes.py index 34abad8561..e8a9b0ec7d 100644 --- a/mayan/apps/ocr/classes.py +++ b/mayan/apps/ocr/classes.py @@ -4,8 +4,6 @@ import logging import os import tempfile -import sh - from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ @@ -18,6 +16,7 @@ from .exceptions import UnpaperError from .literals import ( DEFAULT_OCR_FILE_EXTENSION, DEFAULT_OCR_FILE_FORMAT, UNPAPER_FILE_FORMAT ) +from .models import DocumentPageContent from .parsers import parse_document_page from .parsers.exceptions import ParserError, ParserUnknownFile from .settings import UNPAPER_PATH @@ -34,11 +33,13 @@ class OCRBackendBase(object): for page in document_version.pages.all(): image = page.get_image() - logger.info('Processing page: %d', page.page_number) - page.content = self.execute(file_object=image, language=language) - page.save() + logger.info('Processing page: %d of document version: %s', page.page_number, document_version) + document_page_content, created = DocumentPageContent.objects.get_or_create(document_page=page) + result = self.execute(file_object=image, language=language) + document_page_content.content = self.execute(file_object=image, language=language) + document_page_content.save() image.close() - logger.info('Finished processing page: %d', page.page_number) + logger.info('Finished processing page: %d of document version: %s', page.page_number, document_version) def execute(self, file_object, language=None, transformations=None): if not transformations: diff --git a/mayan/apps/ocr/forms.py b/mayan/apps/ocr/forms.py new file mode 100644 index 0000000000..461c0a8579 --- /dev/null +++ b/mayan/apps/ocr/forms.py @@ -0,0 +1,43 @@ +from __future__ import unicode_literals + +from django import forms +from django.utils.encoding import force_unicode +from django.utils.html import conditional_escape +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _, ugettext + +from common.widgets import TextAreaDiv + +from .models import DocumentPageContent + + +class DocumentContentForm(forms.Form): + """ + Form that concatenates all of a document pages' text content into a + single textarea widget + """ + def __init__(self, *args, **kwargs): + self.document = kwargs.pop('document', None) + super(DocumentContentForm, self).__init__(*args, **kwargs) + content = [] + self.fields['contents'].initial = '' + try: + document_pages = self.document.pages.all() + except AttributeError: + document_pages = [] + + for page in document_pages: + try: + page_content = page.ocr_content.content + except DocumentPageContent.DoesNotExist: + pass + else: + content.append(conditional_escape(force_unicode(page_content))) + content.append('\n\n\n
- %s -

\n\n\n' % (ugettext('Page %(page_number)d') % {'page_number': page.page_number})) + + self.fields['contents'].initial = mark_safe(''.join(content)) + + contents = forms.CharField( + label=_('Contents'), + widget=TextAreaDiv(attrs={'class': 'text_area_div full-height', 'data-height-difference': 360}) + ) diff --git a/mayan/apps/ocr/links.py b/mayan/apps/ocr/links.py index 80973dc417..e6196dedf2 100644 --- a/mayan/apps/ocr/links.py +++ b/mayan/apps/ocr/links.py @@ -5,9 +5,11 @@ from django.utils.translation import ugettext_lazy as _ from navigation import Link from .permissions import ( - PERMISSION_OCR_DOCUMENT, PERMISSION_OCR_DOCUMENT_DELETE + PERMISSION_OCR_CONTENT_VIEW, PERMISSION_OCR_DOCUMENT, + PERMISSION_OCR_DOCUMENT_DELETE ) +link_document_content = Link(permissions=[PERMISSION_OCR_CONTENT_VIEW], text=_('Content'), view='ocr:document_content', args='resolved_object.id') link_document_submit = Link(permissions=[PERMISSION_OCR_DOCUMENT], text=_('Submit to OCR queue'), view='ocr:document_submit', args='object.id') link_document_submit_multiple = Link(text=_('Submit to OCR queue'), view='ocr:document_submit_multiple') link_entry_delete = Link(permissions=[PERMISSION_OCR_DOCUMENT_DELETE], text=_('Delete'), view='ocr:entry_delete', args='object.id') diff --git a/mayan/apps/ocr/migrations/0002_documentpagecontent.py b/mayan/apps/ocr/migrations/0002_documentpagecontent.py new file mode 100644 index 0000000000..85641cc29a --- /dev/null +++ b/mayan/apps/ocr/migrations/0002_documentpagecontent.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0005_auto_20150617_0358'), + ('ocr', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='DocumentPageContent', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('content', models.TextField(verbose_name='Content', blank=True)), + ('document_page', models.OneToOneField(related_name='ocr_content', verbose_name='Document page', to='documents.DocumentPage')), + ], + options={ + 'verbose_name': 'Document page content', + 'verbose_name_plural': 'Document pages contents', + }, + bases=(models.Model,), + ), + ] diff --git a/mayan/apps/ocr/migrations/0003_auto_20150617_0401.py b/mayan/apps/ocr/migrations/0003_auto_20150617_0401.py new file mode 100644 index 0000000000..3a69ebd78e --- /dev/null +++ b/mayan/apps/ocr/migrations/0003_auto_20150617_0401.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +def move_content_from_documents_to_ocr_app(apps, schema_editor): + DocumentPage = apps.get_model('documents', 'DocumentPage') + DocumentPageContent = apps.get_model('ocr', 'DocumentPageContent') + + for document_page in DocumentPage.objects.all(): + document_page_content = DocumentPageContent( + document_page=document_page, content=document_page.content_old + ) + document_page_content.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('ocr', '0002_documentpagecontent'), + ] + + operations = [ + ] + + + operations = [ + migrations.RunPython(move_content_from_documents_to_ocr_app), + ] diff --git a/mayan/apps/ocr/models.py b/mayan/apps/ocr/models.py index e4c1713eb9..cc2210b8b7 100644 --- a/mayan/apps/ocr/models.py +++ b/mayan/apps/ocr/models.py @@ -4,7 +4,7 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from documents.models import DocumentVersion +from documents.models import DocumentVersion, DocumentPage @python_2_unicode_compatible @@ -20,3 +20,19 @@ class DocumentVersionOCRError(models.Model): ordering = ('datetime_submitted',) verbose_name = _('Document Version OCR Error') verbose_name_plural = _('Document Version OCR Errors') + + +@python_2_unicode_compatible +class DocumentPageContent(models.Model): + """ + Model that describes a document page content + """ + document_page = models.OneToOneField(DocumentPage, related_name='ocr_content', verbose_name=_('Document page')) + content = models.TextField(blank=True, verbose_name=_('Content')) + + def __str__(self): + return unicode(self.document_page) + + class Meta: + verbose_name = _('Document page content') + verbose_name_plural = _('Document pages contents') diff --git a/mayan/apps/ocr/parsers/__init__.py b/mayan/apps/ocr/parsers/__init__.py index 303c07eb8b..98d9028692 100644 --- a/mayan/apps/ocr/parsers/__init__.py +++ b/mayan/apps/ocr/parsers/__init__.py @@ -93,7 +93,6 @@ class SlateParser(Parser): raise ParserError('No output') document_page.content = pdf_pages[document_page.page_number - 1] - document_page.page_label = _('Text extracted from PDF') document_page.save() @@ -170,7 +169,6 @@ class PopplerParser(Parser): raise ParserError('No output') document_page.content = output - document_page.page_label = _('Text extracted from PDF') document_page.save() diff --git a/mayan/apps/ocr/permissions.py b/mayan/apps/ocr/permissions.py index c0811fef54..d14cdafcec 100644 --- a/mayan/apps/ocr/permissions.py +++ b/mayan/apps/ocr/permissions.py @@ -7,3 +7,4 @@ from permissions.models import Permission, PermissionNamespace ocr_namespace = PermissionNamespace('ocr', _('OCR')) PERMISSION_OCR_DOCUMENT = Permission.objects.register(ocr_namespace, 'ocr_document', _('Submit documents for OCR')) PERMISSION_OCR_DOCUMENT_DELETE = Permission.objects.register(ocr_namespace, 'ocr_document_delete', _('Delete documents from OCR queue')) +PERMISSION_OCR_CONTENT_VIEW = Permission.objects.register(ocr_namespace, 'ocr_content_view', _('Can view the transcribed text from document')) diff --git a/mayan/apps/ocr/urls.py b/mayan/apps/ocr/urls.py index 02a0bb1120..9805a86ce5 100644 --- a/mayan/apps/ocr/urls.py +++ b/mayan/apps/ocr/urls.py @@ -6,14 +6,15 @@ from .api_views import DocumentVersionOCRView urlpatterns = patterns( 'ocr.views', - url(r'^document/(?P\d+)/submit/$', 'document_submit', (), 'document_submit'), - url(r'^document/multiple/submit/$', 'document_submit_multiple', (), 'document_submit_multiple'), + url(r'^(?P\d+)/content/$', 'document_content', name='document_content'), + url(r'^document/(?P\d+)/submit/$', 'document_submit', name='document_submit'), + url(r'^document/multiple/submit/$', 'document_submit_multiple', name='document_submit_multiple'), - url(r'^all/$', 'entry_list', (), 'entry_list'), - url(r'^(?P\d+)/delete/$', 'entry_delete', (), 'entry_delete'), - url(r'^multiple/delete/$', 'entry_delete_multiple', (), 'entry_delete_multiple'), - url(r'^(?P\d+)/re-queue/$', 'entry_re_queue', (), 'entry_re_queue'), - url(r'^multiple/re-queue/$', 'entry_re_queue_multiple', (), 'entry_re_queue_multiple'), + url(r'^all/$', 'entry_list', name='entry_list'), + url(r'^(?P\d+)/delete/$', 'entry_delete', name='entry_delete'), + url(r'^multiple/delete/$', 'entry_delete_multiple', name='entry_delete_multiple'), + url(r'^(?P\d+)/re-queue/$', 'entry_re_queue', name='entry_re_queue'), + url(r'^multiple/re-queue/$', 'entry_re_queue_multiple', name='entry_re_queue_multiple'), ) api_urls = patterns( diff --git a/mayan/apps/ocr/views.py b/mayan/apps/ocr/views.py index d39a19985b..e9b9e45ba4 100644 --- a/mayan/apps/ocr/views.py +++ b/mayan/apps/ocr/views.py @@ -13,12 +13,36 @@ from acls.models import AccessEntry from documents.models import Document, DocumentVersion from permissions.models import Permission +from .forms import DocumentContentForm from .models import DocumentVersionOCRError from .permissions import ( - PERMISSION_OCR_DOCUMENT, PERMISSION_OCR_DOCUMENT_DELETE + PERMISSION_OCR_CONTENT_VIEW, PERMISSION_OCR_DOCUMENT, + PERMISSION_OCR_DOCUMENT_DELETE ) +def document_content(request, document_id): + document = get_object_or_404(Document, pk=document_id) + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_OCR_CONTENT_VIEW]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_OCR_CONTENT_VIEW, request.user, document) + + document.add_as_recent_document_for_user(request.user) + + content_form = DocumentContentForm(document=document) + + return render_to_response('appearance/generic_form.html', { + 'document': document, + 'form': content_form, + 'hide_labels': True, + 'object': document, + 'read_only': True, + 'title': _('Content of document: %s') % document, + }, context_instance=RequestContext(request)) + + def document_submit(request, pk): document = get_object_or_404(Document, pk=pk) diff --git a/mayan/apps/permissions/links.py b/mayan/apps/permissions/links.py index 5cf4670b56..0ad49b4507 100644 --- a/mayan/apps/permissions/links.py +++ b/mayan/apps/permissions/links.py @@ -15,6 +15,6 @@ link_permission_revoke = Link(permissions=[PERMISSION_PERMISSION_REVOKE], text=_ link_role_create = Link(permissions=[PERMISSION_ROLE_CREATE], text=_('Create new role'), view='permissions:role_create') link_role_delete = Link(permissions=[PERMISSION_ROLE_DELETE], tags='dangerous', text=_('Delete'), view='permissions:role_delete', args='object.id') link_role_edit = Link(permissions=[PERMISSION_ROLE_EDIT], text=_('Edit'), view='permissions:role_edit', args='object.id') -link_role_list = Link(icon='fa fa-child', permissions=[PERMISSION_ROLE_VIEW], text=_('Roles'), view='permissions:role_list') +link_role_list = Link(icon='fa fa-user-secret', permissions=[PERMISSION_ROLE_VIEW], text=_('Roles'), view='permissions:role_list') link_role_members = Link(permissions=[PERMISSION_ROLE_EDIT], text=_('Members'), view='permissions:role_members', args='object.id') link_role_permissions = Link(permissions=[PERMISSION_PERMISSION_GRANT, PERMISSION_PERMISSION_REVOKE], text=_('Role permissions'), view='permissions:role_permissions', args='object.id') diff --git a/mayan/apps/permissions/urls.py b/mayan/apps/permissions/urls.py index b6e6011796..a067a8dbde 100644 --- a/mayan/apps/permissions/urls.py +++ b/mayan/apps/permissions/urls.py @@ -9,15 +9,15 @@ from .views import ( urlpatterns = patterns( 'permissions.views', - url(r'^role/list/$', 'role_list', (), 'role_list'), + url(r'^role/list/$', 'role_list', name='role_list'), url(r'^role/create/$', RoleCreateView.as_view(), name='role_create'), - url(r'^role/(?P\d+)/permissions/$', 'role_permissions', (), 'role_permissions'), + url(r'^role/(?P\d+)/permissions/$', 'role_permissions', name='role_permissions'), url(r'^role/(?P\d+)/edit/$', RoleEditView.as_view(), name='role_edit'), url(r'^role/(?P\d+)/delete/$', RoleDeleteView.as_view(), name='role_delete'), url(r'^role/(?P\d+)/members/$', SetupRoleMembersView.as_view(), name='role_members'), - url(r'^permissions/multiple/grant/$', 'permission_grant', (), 'permission_multiple_grant'), - url(r'^permissions/multiple/revoke/$', 'permission_revoke', (), 'permission_multiple_revoke'), + url(r'^permissions/multiple/grant/$', 'permission_grant', name='permission_multiple_grant'), + url(r'^permissions/multiple/revoke/$', 'permission_revoke', name='permission_multiple_revoke'), ) api_urls = patterns( diff --git a/mayan/apps/smart_settings/links.py b/mayan/apps/smart_settings/links.py index 19188feed2..b918617da5 100644 --- a/mayan/apps/smart_settings/links.py +++ b/mayan/apps/smart_settings/links.py @@ -9,4 +9,4 @@ def is_superuser(context): return context['request'].user.is_staff or context['request'].user.is_superuser -link_check_settings = Link(condition=is_superuser, icon='fa fa-gear', text=_('Settings'), view='settings:setting_list') +link_check_settings = Link(condition=is_superuser, icon='fa fa-sliders', text=_('Settings'), view='settings:setting_list') diff --git a/mayan/apps/smart_settings/urls.py b/mayan/apps/smart_settings/urls.py index 02028cb272..6bc2835333 100644 --- a/mayan/apps/smart_settings/urls.py +++ b/mayan/apps/smart_settings/urls.py @@ -4,5 +4,5 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'smart_settings.views', - url(r'^list/$', 'setting_list', (), 'setting_list'), + url(r'^list/$', 'setting_list', name='setting_list'), ) diff --git a/mayan/apps/sources/apps.py b/mayan/apps/sources/apps.py index 301edd1326..a3294f6627 100644 --- a/mayan/apps/sources/apps.py +++ b/mayan/apps/sources/apps.py @@ -7,6 +7,7 @@ from common import ( MissingItem, menu_front_page, menu_object, menu_secondary, menu_sidebar, menu_setup ) +from common.signals import post_initial_setup from common.utils import encapsulate from converter.links import link_transformation_list from documents.models import Document @@ -14,6 +15,7 @@ from navigation.api import register_model_list_columns from rest_api.classes import APIEndPoint from .classes import StagingFile +from .handlers import create_default_document_source from .links import ( link_document_create_multiple, link_document_create_siblings, link_setup_sources, link_setup_source_create_imap_email, @@ -49,3 +51,5 @@ class SourcesApp(apps.AppConfig): encapsulate(lambda x: staging_file_thumbnail(x, gallery_name='sources:staging_list', title=x.filename, size='100')) }, ]) + + post_initial_setup.connect(create_default_document_source, dispatch_uid='create_default_document_source') diff --git a/mayan/apps/sources/handlers.py b/mayan/apps/sources/handlers.py new file mode 100644 index 0000000000..215408c39a --- /dev/null +++ b/mayan/apps/sources/handlers.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from .literals import SOURCE_UNCOMPRESS_CHOICE_ASK +from .models import WebFormSource + + +def create_default_document_source(sender, **kwargs): + WebFormSource.objects.create(title=_('Default'), uncompress=SOURCE_UNCOMPRESS_CHOICE_ASK) diff --git a/mayan/apps/sources/migrations/0004_auto_20150616_1931.py b/mayan/apps/sources/migrations/0004_auto_20150616_1931.py new file mode 100644 index 0000000000..e7afb13745 --- /dev/null +++ b/mayan/apps/sources/migrations/0004_auto_20150616_1931.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('sources', '0003_sourcelog'), + ] + + operations = [ + migrations.AlterModelOptions( + name='sourcelog', + options={'ordering': ('-datetime',), 'get_latest_by': 'datetime', 'verbose_name': 'Log entry', 'verbose_name_plural': 'Log entries'}, + ), + ] diff --git a/mayan/apps/sources/models.py b/mayan/apps/sources/models.py index 51f991e727..ac15e051f7 100644 --- a/mayan/apps/sources/models.py +++ b/mayan/apps/sources/models.py @@ -352,4 +352,4 @@ class SourceLog(models.Model): verbose_name = _('Log entry') verbose_name_plural = _('Log entries') get_latest_by = 'datetime' - ordering = ['-datetime'] + ordering = ('-datetime',) diff --git a/mayan/apps/sources/urls.py b/mayan/apps/sources/urls.py index 5aa87b7428..01eae6a63e 100644 --- a/mayan/apps/sources/urls.py +++ b/mayan/apps/sources/urls.py @@ -23,16 +23,16 @@ urlpatterns = patterns( # Setup views - url(r'^setup/list/$', 'setup_source_list', (), 'setup_source_list'), - url(r'^setup/(?P\d+)/edit/$', 'setup_source_edit', (), 'setup_source_edit'), + url(r'^setup/list/$', 'setup_source_list', name='setup_source_list'), + url(r'^setup/(?P\d+)/edit/$', 'setup_source_edit', name='setup_source_edit'), url(r'^setup/(?P\d+)/logs/$', SourceLogListView.as_view(), name='setup_source_logs'), - url(r'^setup/(?P\d+)/delete/$', 'setup_source_delete', (), 'setup_source_delete'), - url(r'^setup/(?P\w+)/create/$', 'setup_source_create', (), 'setup_source_create'), + url(r'^setup/(?P\d+)/delete/$', 'setup_source_delete', name='setup_source_delete'), + url(r'^setup/(?P\w+)/create/$', 'setup_source_create', name='setup_source_create'), # Document create views url(r'^create/from/local/multiple/$', DocumentCreateWizard.as_view(), name='document_create_multiple'), - url(r'^(?P\d+)/create/siblings/$', 'document_create_siblings', (), 'document_create_siblings'), + url(r'^(?P\d+)/create/siblings/$', 'document_create_siblings', name='document_create_siblings'), ) api_urls = patterns( diff --git a/mayan/apps/tags/links.py b/mayan/apps/tags/links.py index fb7e7d67cc..7861044b28 100644 --- a/mayan/apps/tags/links.py +++ b/mayan/apps/tags/links.py @@ -17,7 +17,7 @@ link_single_document_multiple_tag_remove = Link(permissions=[PERMISSION_TAG_REMO link_tag_acl_list = Link(permissions=[ACLS_VIEW_ACL], text=_('ACLs'), view='tags:tag_acl_list', args='object.pk') link_tag_attach = Link(permissions=[PERMISSION_TAG_ATTACH], text=_('Attach tag'), view='tags:tag_attach', args='object.pk') link_tag_create = Link(permissions=[PERMISSION_TAG_CREATE], text=_('Create new tag'), view='tags:tag_create') -link_tag_delete = Link(permissions=[PERMISSION_TAG_DELETE], text=_('Delete'), view='tags:tag_delete', args='object.id') +link_tag_delete = Link(permissions=[PERMISSION_TAG_DELETE], tags='dangerous', text=_('Delete'), view='tags:tag_delete', args='object.id') link_tag_edit = Link(permissions=[PERMISSION_TAG_EDIT], text=_('Edit'), view='tags:tag_edit', args='object.id') link_tag_document_list = Link(permissions=[PERMISSION_TAG_REMOVE, PERMISSION_TAG_ATTACH], text=_('Tags'), view='tags:document_tags', args='object.pk') link_tag_list = Link(icon='fa fa-tag', text=_('Tags'), view='tags:tag_list') diff --git a/mayan/apps/tags/urls.py b/mayan/apps/tags/urls.py index 829103af75..f896e7c976 100644 --- a/mayan/apps/tags/urls.py +++ b/mayan/apps/tags/urls.py @@ -10,22 +10,22 @@ from .views import TagTaggedItemListView urlpatterns = patterns( 'tags.views', - url(r'^list/$', 'tag_list', (), 'tag_list'), - url(r'^create/$', 'tag_create', (), 'tag_create'), - url(r'^(?P\d+)/delete/$', 'tag_delete', (), 'tag_delete'), - url(r'^(?P\d+)/edit/$', 'tag_edit', (), 'tag_edit'), + url(r'^list/$', 'tag_list', name='tag_list'), + url(r'^create/$', 'tag_create', name='tag_create'), + url(r'^(?P\d+)/delete/$', 'tag_delete', name='tag_delete'), + url(r'^(?P\d+)/edit/$', 'tag_edit', name='tag_edit'), url(r'^(?P\d+)/documents/$', TagTaggedItemListView.as_view(), name='tag_tagged_item_list'), - url(r'^multiple/delete/$', 'tag_multiple_delete', (), 'tag_multiple_delete'), + url(r'^multiple/delete/$', 'tag_multiple_delete', name='tag_multiple_delete'), - url(r'^multiple/remove/document/(?P\d+)/$', 'single_document_multiple_tag_remove', (), 'single_document_multiple_tag_remove'), - url(r'^multiple/remove/document/multiple/$', 'multiple_documents_selection_tag_remove', (), 'multiple_documents_selection_tag_remove'), + url(r'^multiple/remove/document/(?P\d+)/$', 'single_document_multiple_tag_remove', name='single_document_multiple_tag_remove'), + url(r'^multiple/remove/document/multiple/$', 'multiple_documents_selection_tag_remove', name='multiple_documents_selection_tag_remove'), - url(r'^selection/attach/document/(?P\d+)/$', 'tag_attach', (), 'tag_attach'), - url(r'^selection/attach/document/multiple/$', 'tag_multiple_attach', (), 'multiple_documents_tag_attach'), + url(r'^selection/attach/document/(?P\d+)/$', 'tag_attach', name='tag_attach'), + url(r'^selection/attach/document/multiple/$', 'tag_multiple_attach', name='multiple_documents_tag_attach'), - url(r'^for/document/(?P\d+)/$', 'document_tags', (), 'document_tags'), + url(r'^for/document/(?P\d+)/$', 'document_tags', name='document_tags'), - url(r'^(?P\d+)/acl/list/$', 'tag_acl_list', (), 'tag_acl_list'), + url(r'^(?P\d+)/acl/list/$', 'tag_acl_list', name='tag_acl_list'), ) api_urls = patterns( diff --git a/mayan/apps/user_management/urls.py b/mayan/apps/user_management/urls.py index a7809dd848..3532808f2b 100644 --- a/mayan/apps/user_management/urls.py +++ b/mayan/apps/user_management/urls.py @@ -10,20 +10,20 @@ from .views import GroupMembersView, UserGroupsView urlpatterns = patterns( 'user_management.views', - url(r'^user/list/$', 'user_list', (), 'user_list'), - url(r'^user/add/$', 'user_add', (), 'user_add'), - url(r'^user/(?P\d+)/edit/$', 'user_edit', (), 'user_edit'), - url(r'^user/(?P\d+)/delete/$', 'user_delete', (), 'user_delete'), - url(r'^user/multiple/delete/$', 'user_multiple_delete', (), 'user_multiple_delete'), - url(r'^user/(?P\d+)/set_password/$', 'user_set_password', (), 'user_set_password'), - url(r'^user/multiple/set_password/$', 'user_multiple_set_password', (), 'user_multiple_set_password'), + url(r'^user/list/$', 'user_list', name='user_list'), + url(r'^user/add/$', 'user_add', name='user_add'), + url(r'^user/(?P\d+)/edit/$', 'user_edit', name='user_edit'), + url(r'^user/(?P\d+)/delete/$', 'user_delete', name='user_delete'), + url(r'^user/multiple/delete/$', 'user_multiple_delete', name='user_multiple_delete'), + url(r'^user/(?P\d+)/set_password/$', 'user_set_password', name='user_set_password'), + url(r'^user/multiple/set_password/$', 'user_multiple_set_password', name='user_multiple_set_password'), url(r'^user/(?P\d+)/groups/$', UserGroupsView.as_view(), name='user_groups'), - url(r'^group/list/$', 'group_list', (), 'group_list'), - url(r'^group/add/$', 'group_add', (), 'group_add'), - url(r'^group/(?P\d+)/edit/$', 'group_edit', (), 'group_edit'), - url(r'^group/(?P\d+)/delete/$', 'group_delete', (), 'group_delete'), - url(r'^group/multiple/delete/$', 'group_multiple_delete', (), 'group_multiple_delete'), + url(r'^group/list/$', 'group_list', name='group_list'), + url(r'^group/add/$', 'group_add', name='group_add'), + url(r'^group/(?P\d+)/edit/$', 'group_edit', name='group_edit'), + url(r'^group/(?P\d+)/delete/$', 'group_delete', name='group_delete'), + url(r'^group/multiple/delete/$', 'group_multiple_delete', name='group_multiple_delete'), url(r'^group/(?P\d+)/members/$', GroupMembersView.as_view(), name='group_members'), ) diff --git a/mayan/settings/base.py b/mayan/settings/base.py index bfafacae0b..940aef61a0 100644 --- a/mayan/settings/base.py +++ b/mayan/settings/base.py @@ -53,6 +53,7 @@ INSTALLED_APPS = ( 'django.contrib.staticfiles', # 3rd party 'actstream', + 'autoadmin', 'compressor', 'corsheaders', 'djcelery', @@ -188,10 +189,8 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'mayan', 'media', 'static') # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - ('django.template.loaders.cached.Loader', ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - )), + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader' ) TEMPLATE_CONTEXT_PROCESSORS = ( diff --git a/mayan/settings/development.py b/mayan/settings/development.py index 708ff92212..705b2b5667 100644 --- a/mayan/settings/development.py +++ b/mayan/settings/development.py @@ -18,3 +18,38 @@ INSTALLED_APPS += ( ) WSGI_AUTO_RELOAD = True + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s' + }, + 'intermediate': { + 'format': '%(name)s <%(process)d> [%(levelname)s] "%(funcName)s() %(message)s"' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'handlers': { + 'console':{ + 'level':'DEBUG', + 'class':'logging.StreamHandler', + 'formatter': 'intermediate' + } + }, + 'loggers': { + 'documents': { + 'handlers':['console'], + 'propagate': True, + 'level':'DEBUG', + }, + 'common': { + 'handlers':['console'], + 'propagate': True, + 'level':'DEBUG', + }, + } +} diff --git a/mayan/settings/production.py b/mayan/settings/production.py index fbb5cbf4cd..aae3cd3988 100644 --- a/mayan/settings/production.py +++ b/mayan/settings/production.py @@ -7,3 +7,12 @@ DEBUG = False # Update this accordingly; # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = ['*'] + +TEMPLATE_LOADERS = ( + ('django.template.loaders.cached.Loader', ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + )), +) + +CELERY_ALWAYS_EAGER = False diff --git a/requirements/common.txt b/requirements/common.txt index c47564e403..2cdf48d1c2 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,11 +1,12 @@ Django==1.7.8 -Pillow==2.8.1 +Pillow==2.8.2 celery==3.1.18 cssmin==0.2.0 django-activity-stream==0.5.1 +django-autoadmin==1.0.1 django-celery==3.1.16 django-compressor==1.5 django-cors-headers==1.1.0 @@ -14,7 +15,6 @@ django-pagination==1.0.7 django-model-utils==2.2 django-mptt==0.7.4 django-rest-swagger==0.2.0 -django-solo==1.1.0 django-suit==0.2.13 django-widget-tweaks==1.3 djangorestframework==2.4.4