Start of document_signatures app refactor.
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
- More tests added.
|
||||
- Handle unicode filenames in staging folders.
|
||||
- Add staging file deletion permission.
|
||||
- New document_signature_view permission.
|
||||
|
||||
2.0.2 (2016-02-09)
|
||||
==================
|
||||
|
||||
@@ -2,9 +2,9 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import DocumentVersionSignature
|
||||
|
||||
#from .models import DocumentVersionSignature
|
||||
|
||||
"""
|
||||
@admin.register(DocumentVersionSignature)
|
||||
class DocumentVersionSignatureAdmin(admin.ModelAdmin):
|
||||
def document(self, instance):
|
||||
@@ -20,3 +20,4 @@ class DocumentVersionSignatureAdmin(admin.ModelAdmin):
|
||||
)
|
||||
list_display_links = ('document_version',)
|
||||
search_fields = ('document_version__document__label',)
|
||||
"""
|
||||
|
||||
@@ -6,16 +6,26 @@ from django.apps import apps
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from acls import ModelPermission
|
||||
from common import MayanAppConfig, menu_facet, menu_sidebar
|
||||
from common import (
|
||||
MayanAppConfig, menu_facet, menu_object, menu_secondary, menu_sidebar
|
||||
)
|
||||
from common.widgets import two_state_template
|
||||
from navigation import SourceColumn
|
||||
|
||||
from .hooks import document_pre_open_hook, document_version_post_save_hook
|
||||
from .links import (
|
||||
link_document_signature_delete, link_document_signature_download,
|
||||
link_document_signature_upload, link_document_verify
|
||||
link_document_version_signature_delete,
|
||||
link_document_version_signature_details,
|
||||
link_document_version_signature_download,
|
||||
link_document_version_signature_list,
|
||||
link_document_version_signature_upload,
|
||||
link_document_version_signature_verify
|
||||
)
|
||||
from .permissions import (
|
||||
permission_document_verify, permission_signature_delete,
|
||||
permission_signature_download, permission_signature_upload
|
||||
permission_document_version_signature_delete,
|
||||
permission_document_version_signature_download,
|
||||
permission_document_version_signature_upload,
|
||||
permission_document_version_signature_verify,
|
||||
permission_document_version_signature_view,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -39,30 +49,74 @@ class DocumentSignaturesApp(MayanAppConfig):
|
||||
app_label='documents', model_name='DocumentVersion'
|
||||
)
|
||||
|
||||
DetachedSignature = self.get_model('DetachedSignature')
|
||||
|
||||
EmbeddedSignature = self.get_model('EmbeddedSignature')
|
||||
|
||||
SignatureBaseModel = self.get_model('SignatureBaseModel')
|
||||
|
||||
DocumentVersion.register_post_save_hook(
|
||||
1, document_version_post_save_hook
|
||||
order=1, func=EmbeddedSignature.objects.check_signature
|
||||
)
|
||||
DocumentVersion.register_pre_open_hook(
|
||||
order=1, func=EmbeddedSignature.objects.open_signed
|
||||
)
|
||||
DocumentVersion.register_pre_open_hook(1, document_pre_open_hook)
|
||||
|
||||
ModelPermission.register(
|
||||
model=Document, permissions=(
|
||||
permission_document_verify, permission_signature_delete,
|
||||
permission_signature_download, permission_signature_upload,
|
||||
permission_document_version_signature_delete,
|
||||
permission_document_version_signature_download,
|
||||
permission_document_version_signature_verify,
|
||||
permission_document_version_signature_view,
|
||||
permission_document_version_signature_upload,
|
||||
)
|
||||
)
|
||||
|
||||
menu_facet.bind_links(
|
||||
links=(link_document_verify,), sources=(Document,)
|
||||
SourceColumn(
|
||||
source=SignatureBaseModel, label=_('Date'), attribute='date'
|
||||
)
|
||||
SourceColumn(
|
||||
source=SignatureBaseModel, label=_('Key ID'), attribute='key_id'
|
||||
)
|
||||
SourceColumn(
|
||||
source=SignatureBaseModel, label=_('Signature ID'),
|
||||
func=lambda context: context['object'].signature_id or _('None')
|
||||
)
|
||||
SourceColumn(
|
||||
source=SignatureBaseModel, label=_('Public key ID'),
|
||||
func=lambda context: context['object'].public_key_fingerprint or _('None')
|
||||
)
|
||||
SourceColumn(
|
||||
source=SignatureBaseModel, label=_('Is embedded?'),
|
||||
func=lambda context: two_state_template(
|
||||
SignatureBaseModel.objects.get_subclass(
|
||||
pk=context['object'].pk
|
||||
).is_embedded
|
||||
)
|
||||
)
|
||||
SourceColumn(
|
||||
source=SignatureBaseModel, label=_('Is detached?'),
|
||||
func=lambda context: two_state_template(
|
||||
SignatureBaseModel.objects.get_subclass(
|
||||
pk=context['object'].pk
|
||||
).is_detached
|
||||
)
|
||||
)
|
||||
|
||||
menu_object.bind_links(
|
||||
links=(link_document_version_signature_list,),
|
||||
sources=(DocumentVersion,)
|
||||
)
|
||||
menu_object.bind_links(
|
||||
links=(
|
||||
link_document_version_signature_details,
|
||||
link_document_version_signature_download,
|
||||
link_document_version_signature_delete,
|
||||
), sources=(SignatureBaseModel,)
|
||||
)
|
||||
menu_sidebar.bind_links(
|
||||
links=(
|
||||
link_document_signature_upload,
|
||||
link_document_signature_download,
|
||||
link_document_signature_delete
|
||||
), sources=(
|
||||
'signatures:document_verify',
|
||||
'signatures:document_signature_upload',
|
||||
'signatures:document_signature_download',
|
||||
'signatures:document_signature_delete'
|
||||
)
|
||||
link_document_version_signature_upload,
|
||||
link_document_version_signature_verify,
|
||||
), sources=(DocumentVersion,)
|
||||
)
|
||||
|
||||
@@ -3,8 +3,49 @@ from __future__ import unicode_literals
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.forms import DetailForm
|
||||
|
||||
from .models import SignatureBaseModel
|
||||
|
||||
|
||||
class DetachedSignatureForm(forms.Form):
|
||||
file = forms.FileField(
|
||||
label=_('Signature file'),
|
||||
)
|
||||
|
||||
|
||||
class DocumentVersionSignatureDetailForm(DetailForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
extra_fields = (
|
||||
{'label': _('Is embedded?'), 'field': 'is_embedded'},
|
||||
{'label': _('Date'), 'field': 'date'},
|
||||
{'label': _('Key ID'), 'field': 'key_id'},
|
||||
)
|
||||
|
||||
kwargs['extra_fields'] = extra_fields
|
||||
super(DocumentVersionSignatureDetailForm, self).__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
fields = ()
|
||||
model = SignatureBaseModel
|
||||
|
||||
|
||||
"""
|
||||
{
|
||||
'label': _('User ID'),
|
||||
'field': lambda x: escape(instance.user_id),
|
||||
},
|
||||
{
|
||||
'label': _('Creation date'), 'field': 'creation_date',
|
||||
'widget': forms.widgets.DateInput
|
||||
},
|
||||
{
|
||||
'label': _('Expiration date'),
|
||||
'field': lambda x: instance.expiration_date or _('None'),
|
||||
'widget': forms.widgets.DateInput
|
||||
},
|
||||
{'label': _('Fingerprint'), 'field': 'fingerprint'},
|
||||
{'label': _('Length'), 'field': 'length'},
|
||||
{'label': _('Algorithm'), 'field': 'algorithm'},
|
||||
{'label': _('Type'), 'field': lambda x: instance.get_key_type_display()},
|
||||
"""
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import io
|
||||
import logging
|
||||
|
||||
from django.apps import apps
|
||||
from django_gpg.exceptions import DecryptionError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def document_pre_open_hook(file_object, instance):
|
||||
logger.debug('instance: %s', instance)
|
||||
|
||||
DocumentVersionSignature = apps.get_model(
|
||||
app_label='document_signatures', model_name='DocumentVersionSignature'
|
||||
)
|
||||
|
||||
Key = apps.get_model(
|
||||
app_label='django_gpg', model_name='Key'
|
||||
)
|
||||
|
||||
if DocumentVersionSignature.objects.has_embedded_signature(document_version=instance):
|
||||
# If it has an embedded signature, decrypt
|
||||
try:
|
||||
result = Key.objects.decrypt_file(file_object=file_object)
|
||||
# gpg return a string, turn it into a file like object
|
||||
except DecryptionError:
|
||||
# At least return the original raw content
|
||||
file_object.seek(0)
|
||||
return file_object
|
||||
else:
|
||||
file_object.close()
|
||||
return io.BytesIO(result)
|
||||
else:
|
||||
return file_object
|
||||
|
||||
|
||||
def document_version_post_save_hook(instance):
|
||||
logger.debug('instance: %s', instance)
|
||||
|
||||
DocumentVersionSignature = apps.get_model(
|
||||
app_label='document_signatures', model_name='DocumentVersionSignature'
|
||||
)
|
||||
|
||||
try:
|
||||
document_signature = DocumentVersionSignature.objects.get(
|
||||
document_version=instance
|
||||
)
|
||||
except DocumentVersionSignature.DoesNotExist:
|
||||
document_signature = DocumentVersionSignature.objects.create(
|
||||
document_version=instance
|
||||
)
|
||||
document_signature.check_for_embedded_signature()
|
||||
@@ -6,50 +6,57 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from navigation import Link
|
||||
|
||||
from .permissions import (
|
||||
permission_document_verify, permission_signature_delete,
|
||||
permission_signature_download, permission_signature_upload,
|
||||
permission_document_version_signature_delete,
|
||||
permission_document_version_signature_download,
|
||||
permission_document_version_signature_upload,
|
||||
permission_document_version_signature_verify,
|
||||
permission_document_version_signature_view
|
||||
)
|
||||
|
||||
|
||||
def can_upload_detached_signature(context):
|
||||
DocumentVersionSignature = apps.get_model(
|
||||
app_label='document_signatures', model_name='DocumentVersionSignature'
|
||||
def is_detached_signature(context):
|
||||
SignatureBaseModel = apps.get_model(
|
||||
app_label='document_signatures', model_name='SignatureBaseModel'
|
||||
)
|
||||
|
||||
return not DocumentVersionSignature.objects.has_detached_signature(
|
||||
context['object'].latest_version
|
||||
) and not DocumentVersionSignature.objects.has_embedded_signature(
|
||||
context['object'].latest_version
|
||||
)
|
||||
return SignatureBaseModel.objects.select_subclasses().get(
|
||||
pk=context['object'].pk
|
||||
).is_detached
|
||||
|
||||
|
||||
def can_delete_detached_signature(context):
|
||||
DocumentVersionSignature = apps.get_model(
|
||||
app_label='document_signatures', model_name='DocumentVersionSignature'
|
||||
link_document_version_signature_delete = Link(
|
||||
condition=is_detached_signature,
|
||||
#permissions=(permission_document_version_signature_delete,),
|
||||
tags='dangerous', text=_('Delete'),
|
||||
view='signatures:document_version_signature_delete',
|
||||
args='resolved_object.pk'
|
||||
)
|
||||
|
||||
return DocumentVersionSignature.objects.has_detached_signature(
|
||||
context['object'].latest_version
|
||||
link_document_version_signature_details = Link(
|
||||
#permissions=(permission_document_version_signature_view,),
|
||||
text=_('Details'),
|
||||
view='signatures:document_version_signature_details',
|
||||
args='resolved_object.pk'
|
||||
)
|
||||
|
||||
|
||||
link_document_signature_delete = Link(
|
||||
condition=can_delete_detached_signature,
|
||||
permissions=(permission_signature_delete,), tags='dangerous',
|
||||
text=_('Delete signature'), view='signatures:document_signature_delete',
|
||||
args='object.pk'
|
||||
link_document_version_signature_list = Link(
|
||||
#permissions=(permission_document_version_signature_view,),
|
||||
text=_('Signature list'),
|
||||
view='signatures:document_version_signature_list',
|
||||
args='resolved_object.pk'
|
||||
)
|
||||
link_document_signature_download = Link(
|
||||
condition=can_delete_detached_signature, text=_('Download signature'),
|
||||
view='signatures:document_signature_download', args='object.pk',
|
||||
permissions=(permission_signature_download,)
|
||||
link_document_version_signature_download = Link(
|
||||
condition=is_detached_signature,
|
||||
text=_('Download'),
|
||||
view='signatures:document_signature_download', args='resolved_object.pk',
|
||||
#permissions=(permission_document_version_signature_download,)
|
||||
)
|
||||
link_document_signature_upload = Link(
|
||||
condition=can_upload_detached_signature,
|
||||
permissions=(permission_signature_upload,), text=_('Upload signature'),
|
||||
view='signatures:document_signature_upload', args='object.pk'
|
||||
link_document_version_signature_upload = Link(
|
||||
#permissions=(permission_document_version_signature_upload,),
|
||||
text=_('Upload signature'), view='signatures:document_version_signature_upload',
|
||||
args='resolved_object.pk'
|
||||
)
|
||||
link_document_verify = Link(
|
||||
icon='fa fa-certificate', permissions=(permission_document_verify,),
|
||||
text=_('Signatures'), view='signatures:document_verify', args='object.pk'
|
||||
link_document_version_signature_verify = Link(
|
||||
icon='fa fa-certificate',
|
||||
#permissions=(permission_document_version_signature_verify,),
|
||||
text=_('Verify signatures'), view='signatures:document_verify',
|
||||
args='resolved_object.pk'
|
||||
)
|
||||
|
||||
@@ -4,73 +4,66 @@ import logging
|
||||
|
||||
from django.db import models
|
||||
|
||||
from django_gpg.exceptions import VerificationError
|
||||
from django_gpg.exceptions import DecryptionError, VerificationError
|
||||
from django_gpg.models import Key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DocumentVersionSignatureManager(models.Manager):
|
||||
def get_document_signature(self, document_version):
|
||||
document_signature, created = self.model.objects.get_or_create(
|
||||
class DetachedSignatureManager(models.Manager):
|
||||
def upload_signature(self, document_version, signature_file):
|
||||
with document_version.open() as file_object:
|
||||
try:
|
||||
verify_result = Key.objects.verify_file(
|
||||
file_object=file_object, signature_file=signature_file
|
||||
)
|
||||
except VerificationError:
|
||||
# Not signed
|
||||
pass
|
||||
else:
|
||||
instance = self.create(
|
||||
document_version=document_version,
|
||||
date=verify_result.date,
|
||||
key_id=verify_result.key_id,
|
||||
signature_id=verify_result.signature_id,
|
||||
public_key_fingerprint=verify_result.pubkey_fingerprint,
|
||||
)
|
||||
|
||||
return document_signature
|
||||
|
||||
def add_detached_signature(self, document_version, detached_signature):
|
||||
document_signature = self.get_document_signature(
|
||||
document_version=document_version
|
||||
)
|
||||
class EmbeddedSignatureManager(models.Manager):
|
||||
def check_signature(self, document_version):
|
||||
logger.debug('checking for embedded signature')
|
||||
|
||||
if document_signature.has_embedded_signature:
|
||||
raise Exception(
|
||||
'Document version already has an embedded signature'
|
||||
)
|
||||
else:
|
||||
if document_signature.signature_file:
|
||||
logger.debug('Existing detached signature')
|
||||
document_signature.delete_detached_signature_file()
|
||||
document_signature.signature_file = None
|
||||
document_signature.save()
|
||||
|
||||
document_signature.signature_file = detached_signature
|
||||
document_signature.save()
|
||||
|
||||
def has_detached_signature(self, document_version):
|
||||
with document_version.open() as file_object:
|
||||
try:
|
||||
document_signature = self.get_document_signature(
|
||||
document_version=document_version
|
||||
verify_result = Key.objects.verify_file(file_object=file_object)
|
||||
except VerificationError:
|
||||
# Not signed
|
||||
pass
|
||||
else:
|
||||
instance = self.create(
|
||||
document_version=document_version,
|
||||
date=verify_result.date,
|
||||
key_id=verify_result.key_id,
|
||||
signature_id=verify_result.signature_id,
|
||||
public_key_fingerprint=verify_result.pubkey_fingerprint,
|
||||
)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
if document_signature.signature_file:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def has_embedded_signature(self, document_version):
|
||||
logger.debug('document_version: %s', document_version)
|
||||
|
||||
def open_signed(self, file_object, document_version):
|
||||
for signature in self.filter(document_version=document_version):
|
||||
try:
|
||||
document_signature = self.get_document_signature(
|
||||
document_version=document_version
|
||||
return self.open_signed(
|
||||
file_object=Key.objects.decrypt_file(
|
||||
file_object=file_object
|
||||
), document_version=document_version
|
||||
)
|
||||
except ValueError:
|
||||
return False
|
||||
except DecryptionError:
|
||||
file_object.seek(0)
|
||||
return file_object
|
||||
else:
|
||||
return document_signature.has_embedded_signature
|
||||
|
||||
def detached_signature(self, document_version):
|
||||
document_signature = self.get_document_signature(
|
||||
document_version=document_version
|
||||
)
|
||||
|
||||
return document_signature.signature_file.storage.open(
|
||||
document_signature.signature_file.name
|
||||
)
|
||||
return file_object
|
||||
|
||||
"""
|
||||
def verify_signature(self, document_version):
|
||||
document_version_descriptor = document_version.open(raw=True)
|
||||
detached_signature = None
|
||||
@@ -91,14 +84,4 @@ class DocumentVersionSignatureManager(models.Manager):
|
||||
document_version_descriptor.close()
|
||||
if detached_signature:
|
||||
detached_signature.close()
|
||||
|
||||
def clear_detached_signature(self, document_version):
|
||||
document_signature = self.get_document_signature(
|
||||
document_version=document_version
|
||||
)
|
||||
if not document_signature.signature_file:
|
||||
raise Exception('document doesn\'t have a detached signature')
|
||||
|
||||
document_signature.delete_detached_signature_file()
|
||||
document_signature.signature_file = None
|
||||
document_signature.save()
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import storage.backends.filebasedstorage
|
||||
import document_signatures.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documents', '0033_auto_20160325_0052'),
|
||||
('document_signatures', '0002_auto_20150608_1902'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SignatureBaseModel',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('date', models.DateField(null=True, verbose_name='Date signed', blank=True)),
|
||||
('key_id', models.CharField(max_length=40, verbose_name='Key ID')),
|
||||
('signature_id', models.CharField(max_length=64, null=True, verbose_name='Signature ID', blank=True)),
|
||||
('public_key_fingerprint', models.CharField(verbose_name='Public key fingerprint', unique=True, max_length=40, editable=False)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document version signature',
|
||||
'verbose_name_plural': 'Document version signatures',
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='documentversionsignature',
|
||||
name='has_embedded_signature',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='documentversionsignature',
|
||||
name='date',
|
||||
field=models.DateField(null=True, verbose_name='Date signed', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='documentversionsignature',
|
||||
name='signature_id',
|
||||
field=models.CharField(max_length=64, null=True, verbose_name='Signature ID', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='documentversionsignature',
|
||||
name='document_version',
|
||||
field=models.ForeignKey(related_name='signature', editable=False, to='documents.DocumentVersion', verbose_name='Document version'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DetachedSignature',
|
||||
fields=[
|
||||
('signaturebasemodel_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='document_signatures.SignatureBaseModel')),
|
||||
('signature_file', models.FileField(storage=storage.backends.filebasedstorage.FileBasedStorage(), upload_to=document_signatures.models.upload_to, null=True, verbose_name='Signature file', blank=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document version detached signature',
|
||||
'verbose_name_plural': 'Document version detached signatures',
|
||||
},
|
||||
bases=('document_signatures.signaturebasemodel',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EmbeddedSignature',
|
||||
fields=[
|
||||
('signaturebasemodel_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='document_signatures.SignatureBaseModel')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document version embedded signature',
|
||||
'verbose_name_plural': 'Document version embedded signatures',
|
||||
},
|
||||
bases=('document_signatures.signaturebasemodel',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signaturebasemodel',
|
||||
name='document_version',
|
||||
field=models.ForeignKey(related_name='signaturebasemodel', editable=False, to='documents.DocumentVersion', verbose_name='Document version'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('document_signatures', '0003_auto_20160325_0052'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='documentversionsignature',
|
||||
name='document_version',
|
||||
field=models.ForeignKey(editable=False, to='documents.DocumentVersion', verbose_name='Document version'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='signaturebasemodel',
|
||||
name='date',
|
||||
field=models.DateField(verbose_name='Date signed', null=True, editable=False, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='signaturebasemodel',
|
||||
name='document_version',
|
||||
field=models.ForeignKey(related_name='signatures', editable=False, to='documents.DocumentVersion', verbose_name='Document version'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='signaturebasemodel',
|
||||
name='public_key_fingerprint',
|
||||
field=models.CharField(null=True, editable=False, max_length=40, blank=True, unique=True, verbose_name='Public key fingerprint'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='signaturebasemodel',
|
||||
name='signature_id',
|
||||
field=models.CharField(verbose_name='Signature ID', max_length=64, null=True, editable=False, blank=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('document_signatures', '0004_auto_20160325_0418'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='documentversionsignature',
|
||||
name='document_version',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='DocumentVersionSignature',
|
||||
),
|
||||
]
|
||||
@@ -1,16 +1,21 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import date
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django_gpg.exceptions import DecryptionError
|
||||
from model_utils.managers import InheritanceManager
|
||||
|
||||
from django_gpg.exceptions import DecryptionError, VerificationError
|
||||
from django_gpg.models import Key
|
||||
from documents.models import DocumentVersion
|
||||
|
||||
from .managers import DocumentVersionSignatureManager
|
||||
from .managers import EmbeddedSignatureManager, DetachedSignatureManager
|
||||
from .runtime import storage_backend
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -20,39 +25,71 @@ def upload_to(*args, **kwargs):
|
||||
return unicode(uuid.uuid4())
|
||||
|
||||
|
||||
class DocumentVersionSignature(models.Model):
|
||||
"""
|
||||
Model that describes a document version signature properties
|
||||
"""
|
||||
@python_2_unicode_compatible
|
||||
class SignatureBaseModel(models.Model):
|
||||
document_version = models.ForeignKey(
|
||||
DocumentVersion, editable=False, verbose_name=_('Document version')
|
||||
DocumentVersion, editable=False, related_name='signatures',
|
||||
verbose_name=_('Document version')
|
||||
)
|
||||
signature_file = models.FileField(
|
||||
blank=True, null=True, storage=storage_backend, upload_to=upload_to,
|
||||
verbose_name=_('Signature file')
|
||||
# Basic fields
|
||||
date = models.DateField(
|
||||
blank=True, editable=False, null=True, verbose_name=_('Date signed')
|
||||
)
|
||||
has_embedded_signature = models.BooleanField(
|
||||
default=False, verbose_name=_('Has embedded signature')
|
||||
key_id = models.CharField(max_length=40, verbose_name=_('Key ID'))
|
||||
# With proper key
|
||||
signature_id = models.CharField(
|
||||
blank=True, editable=False, null=True, max_length=64,
|
||||
verbose_name=_('Signature ID')
|
||||
)
|
||||
public_key_fingerprint = models.CharField(
|
||||
blank=True, editable=False, null=True, max_length=40, unique=True,
|
||||
verbose_name=_('Public key fingerprint')
|
||||
)
|
||||
|
||||
objects = DocumentVersionSignatureManager()
|
||||
|
||||
def check_for_embedded_signature(self):
|
||||
logger.debug('checking for embedded signature')
|
||||
|
||||
with self.document_version.open(raw=True) as file_object:
|
||||
try:
|
||||
Key.objects.decrypt_file(file_object=file_object)
|
||||
except DecryptionError:
|
||||
self.has_embedded_signature = False
|
||||
else:
|
||||
self.has_embedded_signature = True
|
||||
|
||||
self.save()
|
||||
|
||||
def delete_detached_signature_file(self):
|
||||
self.signature_file.storage.delete(self.signature_file.name)
|
||||
objects = InheritanceManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Document version signature')
|
||||
verbose_name_plural = _('Document version signatures')
|
||||
|
||||
def __str__(self):
|
||||
return self.signature_id or '{} - {}'.format(self.date, self.key_id)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
'document_signatures:document_version_signature_detail',
|
||||
args=(self.pk,)
|
||||
)
|
||||
|
||||
@property
|
||||
def is_detached(self):
|
||||
return hasattr(self, 'signature_file')
|
||||
|
||||
@property
|
||||
def is_embedded(self):
|
||||
return not hasattr(self, 'signature_file')
|
||||
|
||||
|
||||
class EmbeddedSignature(SignatureBaseModel):
|
||||
objects = EmbeddedSignatureManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Document version embedded signature')
|
||||
verbose_name_plural = _('Document version embedded signatures')
|
||||
|
||||
|
||||
class DetachedSignature(SignatureBaseModel):
|
||||
signature_file = models.FileField(
|
||||
blank=True, null=True, storage=storage_backend, upload_to=upload_to,
|
||||
verbose_name=_('Signature file')
|
||||
)
|
||||
|
||||
objects = DetachedSignatureManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Document version detached signature')
|
||||
verbose_name_plural = _('Document version detached signatures')
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.signature_file.storage.delete(self.signature_file.name)
|
||||
super(DetachedSignature, self).delete(*args, **kwargs)
|
||||
|
||||
@@ -8,15 +8,23 @@ namespace = PermissionNamespace(
|
||||
'document_signatures', _('Document signatures')
|
||||
)
|
||||
|
||||
permission_document_verify = namespace.add_permission(
|
||||
name='document_verify', label=_('Verify document signatures')
|
||||
permission_document_version_signature_view = namespace.add_permission(
|
||||
name='document_version_signature_view',
|
||||
label=_('View details of document signatures')
|
||||
)
|
||||
permission_signature_delete = namespace.add_permission(
|
||||
name='signature_delete', label=_('Delete detached signatures')
|
||||
permission_document_version_signature_verify = namespace.add_permission(
|
||||
name='document_version_signature_verify',
|
||||
label=_('Verify document signatures')
|
||||
)
|
||||
permission_signature_download = namespace.add_permission(
|
||||
name='signature_download', label=_('Download detached signatures')
|
||||
permission_document_version_signature_delete = namespace.add_permission(
|
||||
name='document_version_signature_delete',
|
||||
label=_('Delete detached signatures')
|
||||
)
|
||||
permission_signature_upload = namespace.add_permission(
|
||||
name='signature_upload', label=_('Upload detached signatures')
|
||||
permission_document_version_signature_download = namespace.add_permission(
|
||||
name='document_version_signature_download',
|
||||
label=_('Download detached document signatures')
|
||||
)
|
||||
permission_document_version_signature_upload = namespace.add_permission(
|
||||
name='document_version_signature_upload',
|
||||
label=_('Upload detached document signatures')
|
||||
)
|
||||
|
||||
@@ -4,14 +4,13 @@ import os
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.base import File
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_gpg.models import Key
|
||||
from documents.models import DocumentType
|
||||
from documents.tests import TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE
|
||||
|
||||
from ..models import DocumentVersionSignature
|
||||
from ..models import DetachedSignature, EmbeddedSignature
|
||||
|
||||
TEST_SIGNED_DOCUMENT_PATH = os.path.join(
|
||||
settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg'
|
||||
@@ -23,6 +22,7 @@ TEST_KEY_FILE = os.path.join(
|
||||
settings.BASE_DIR, 'contrib', 'sample_documents',
|
||||
'key0x5F3F7F75D210724D.asc'
|
||||
)
|
||||
TEST_KEY_ID = '5F3F7F75D210724D'
|
||||
|
||||
|
||||
@override_settings(OCR_AUTO_OCR=False)
|
||||
@@ -32,28 +32,82 @@ class DocumentTestCase(TestCase):
|
||||
label=TEST_DOCUMENT_TYPE
|
||||
)
|
||||
|
||||
with open(TEST_DOCUMENT_PATH) as file_object:
|
||||
self.document = self.document_type.new_document(
|
||||
file_object=File(file_object), label='mayan_11_1.pdf'
|
||||
)
|
||||
|
||||
with open(TEST_KEY_FILE) as file_object:
|
||||
Key.objects.create(key_data=file_object.read())
|
||||
|
||||
def tearDown(self):
|
||||
self.document_type.delete()
|
||||
|
||||
def test_document_no_signature(self):
|
||||
self.assertEqual(
|
||||
DocumentVersionSignature.objects.has_detached_signature(
|
||||
self.document.latest_version
|
||||
), False
|
||||
def test_embedded_signature(self):
|
||||
with open(TEST_SIGNED_DOCUMENT_PATH) as file_object:
|
||||
signed_document = self.document_type.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
def test_new_document_version_signed(self):
|
||||
self.assertEqual(EmbeddedSignature.objects.count(), 1)
|
||||
|
||||
signature = EmbeddedSignature.objects.first()
|
||||
|
||||
self.assertEqual(
|
||||
signature.document_version, signed_document.latest_version
|
||||
)
|
||||
self.assertEqual(signature.key_id, TEST_KEY_ID)
|
||||
|
||||
def test_embedded_signature_with_key(self):
|
||||
with open(TEST_KEY_FILE) as file_object:
|
||||
key = Key.objects.create(key_data=file_object.read())
|
||||
|
||||
with open(TEST_SIGNED_DOCUMENT_PATH) as file_object:
|
||||
self.document.new_version(
|
||||
file_object=File(file_object), comment='test comment 1'
|
||||
self.signed_document = self.document_type.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
self.assertEqual(EmbeddedSignature.objects.count(), 1)
|
||||
|
||||
signature = EmbeddedSignature.objects.first()
|
||||
|
||||
self.assertEqual(
|
||||
signature.document_version,
|
||||
self.signed_document.latest_version
|
||||
)
|
||||
self.assertEqual(signature.key_id, TEST_KEY_ID)
|
||||
self.assertEqual(signature.public_key_fingerprint, key.fingerprint)
|
||||
|
||||
def test_detached_signature(self):
|
||||
with open(TEST_DOCUMENT_PATH) as file_object:
|
||||
document = self.document_type.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
with open(TEST_SIGNATURE_FILE_PATH) as file_object:
|
||||
DetachedSignature.objects.upload_signature(
|
||||
document_version=document.latest_version,
|
||||
signature_file=file_object
|
||||
)
|
||||
|
||||
self.assertEqual(DetachedSignature.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
DetachedSignature.objects.first().document_version,
|
||||
document.latest_version
|
||||
)
|
||||
self.assertEqual(DetachedSignature.objects.first().key_id, TEST_KEY_ID)
|
||||
|
||||
# TODO: test_verify_signature_after_new_key(self):
|
||||
|
||||
def test_document_no_signature(self):
|
||||
with open(TEST_DOCUMENT_PATH) as file_object:
|
||||
document = self.document_type.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
self.assertEqual(EmbeddedSignature.objects.count(), 0)
|
||||
|
||||
def test_new_signed_version(self):
|
||||
with open(TEST_DOCUMENT_PATH) as file_object:
|
||||
document = self.document_type.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
with open(TEST_SIGNED_DOCUMENT_PATH) as file_object:
|
||||
signed_version = document.new_version(
|
||||
file_object=file_object, comment='test comment 1'
|
||||
)
|
||||
|
||||
# Artifical delay since MySQL doesn't store microsecond data in
|
||||
@@ -61,40 +115,9 @@ class DocumentTestCase(TestCase):
|
||||
# is the latest.
|
||||
time.sleep(2)
|
||||
|
||||
self.assertEqual(
|
||||
DocumentVersionSignature.objects.has_detached_signature(
|
||||
self.document.latest_version
|
||||
), False
|
||||
)
|
||||
self.assertEqual(
|
||||
DocumentVersionSignature.objects.verify_signature(
|
||||
self.document.latest_version
|
||||
).status, SIGNATURE_STATE_VALID
|
||||
)
|
||||
self.assertEqual(EmbeddedSignature.objects.count(), 1)
|
||||
|
||||
def test_detached_signatures(self):
|
||||
with open(TEST_DOCUMENT_PATH) as file_object:
|
||||
self.document.new_version(
|
||||
file_object=File(file_object), comment='test comment 2'
|
||||
)
|
||||
signature = EmbeddedSignature.objects.first()
|
||||
|
||||
# GPGVerificationError
|
||||
self.assertEqual(DocumentVersionSignature.objects.verify_signature(
|
||||
self.document.latest_version), None
|
||||
)
|
||||
|
||||
with open(TEST_SIGNATURE_FILE_PATH, 'rb') as file_object:
|
||||
DocumentVersionSignature.objects.add_detached_signature(
|
||||
self.document.latest_version, File(file_object)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
DocumentVersionSignature.objects.has_detached_signature(
|
||||
self.document.latest_version
|
||||
), True
|
||||
)
|
||||
self.assertEqual(
|
||||
DocumentVersionSignature.objects.verify_signature(
|
||||
self.document.latest_version
|
||||
).status, SIGNATURE_STATE_VALID
|
||||
)
|
||||
self.assertEqual(signature.document_version, signed_version)
|
||||
self.assertEqual(signature.key_id, TEST_KEY_ID)
|
||||
|
||||
@@ -2,22 +2,40 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from .views import (
|
||||
DocumentVersionSignatureDeleteView, DocumentVersionSignatureDetailView,
|
||||
DocumentVersionSignatureListView
|
||||
)
|
||||
|
||||
urlpatterns = patterns(
|
||||
'document_signatures.views',
|
||||
url(
|
||||
r'^verify/(?P<document_pk>\d+)/$', 'document_verify',
|
||||
name='document_verify'
|
||||
r'^(?P<pk>\d+)/details/$',
|
||||
DocumentVersionSignatureDetailView.as_view(),
|
||||
name='document_version_signature_details'
|
||||
),
|
||||
url(
|
||||
r'^upload/signature/(?P<document_pk>\d+)/$',
|
||||
'document_signature_upload', name='document_signature_upload'
|
||||
r'^signature/(?P<pk>\d+)/download/$',
|
||||
'document_signature_download',
|
||||
name='document_version_signature_download'
|
||||
),
|
||||
url(
|
||||
r'^download/signature/(?P<document_pk>\d+)/$',
|
||||
'document_signature_download', name='document_signature_download'
|
||||
r'^document/version/(?P<pk>\d+)/signatures/list/$',
|
||||
DocumentVersionSignatureListView.as_view(),
|
||||
name='document_version_signature_list'
|
||||
),
|
||||
url(
|
||||
r'^document/(?P<document_pk>\d+)/signature/delete/$',
|
||||
'document_signature_delete', name='document_signature_delete'
|
||||
r'^documents/version/(?P<document_pk>\d+)/signature/verify/$',
|
||||
'document_verify', name='document_version_signature_verify'
|
||||
),
|
||||
url(
|
||||
r'^documents/version/(?P<pk>\d+)/signature/upload/$',
|
||||
'document_version_signature_upload',
|
||||
name='document_version_signature_upload'
|
||||
),
|
||||
url(
|
||||
r'^signature/(?P<pk>\d+)/delete/$',
|
||||
DocumentVersionSignatureDeleteView.as_view(),
|
||||
name='document_version_signature_delete'
|
||||
),
|
||||
)
|
||||
|
||||
@@ -14,21 +14,95 @@ from django.utils.html import escape
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from acls.models import AccessControlList
|
||||
from common.generics import (
|
||||
SingleObjectDeleteView, SingleObjectDetailView, SingleObjectListView
|
||||
)
|
||||
from django_gpg.literals import SIGNATURE_STATE_NONE, SIGNATURE_STATES
|
||||
from documents.models import Document
|
||||
from documents.models import Document, DocumentVersion
|
||||
from filetransfers.api import serve_file
|
||||
from permissions import Permission
|
||||
|
||||
from .forms import DetachedSignatureForm
|
||||
from .models import DocumentVersionSignature
|
||||
from .forms import DetachedSignatureForm, DocumentVersionSignatureDetailForm
|
||||
from .models import DetachedSignature, SignatureBaseModel
|
||||
from .permissions import (
|
||||
permission_document_verify, permission_signature_upload,
|
||||
permission_signature_download, permission_signature_delete
|
||||
permission_document_version_signature_view,
|
||||
permission_document_version_signature_verify,
|
||||
permission_document_version_signature_upload,
|
||||
permission_document_version_signature_download,
|
||||
permission_document_version_signature_delete
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DocumentVersionSignatureDeleteView(SingleObjectDeleteView):
|
||||
model = DetachedSignature
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'document': self.get_object().document_version.document,
|
||||
'document_version': self.get_object().document_version,
|
||||
'navigation_object_list': ('document', 'document_version', 'signature'),
|
||||
'signature': self.get_object(),
|
||||
'title': _('Delete detached signature: %s') % self.get_object()
|
||||
}
|
||||
|
||||
def get_post_action_redirect(self):
|
||||
return reverse(
|
||||
'signatures:document_version_signature_list',
|
||||
args=(self.get_object().document_version.pk,)
|
||||
)
|
||||
|
||||
|
||||
class DocumentVersionSignatureDetailView(SingleObjectDetailView):
|
||||
form_class = DocumentVersionSignatureDetailForm
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'document': self.get_object().document_version.document,
|
||||
'document_version': self.get_object().document_version,
|
||||
'signature': self.get_object(),
|
||||
'navigation_object_list': ('document', 'document_version', 'signature'),
|
||||
'hide_object': True,
|
||||
'title': _(
|
||||
'Details for signature: %s'
|
||||
) % self.get_object(),
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return SignatureBaseModel.objects.select_subclasses()
|
||||
|
||||
|
||||
class DocumentVersionSignatureListView(SingleObjectListView):
|
||||
def get_document_version(self):
|
||||
return get_object_or_404(DocumentVersion, pk=self.kwargs['pk'])
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'document': self.get_document_version().document,
|
||||
'document_version': self.get_document_version(),
|
||||
'navigation_object_list': ('document', 'document_version'),
|
||||
'hide_object': True,
|
||||
'title': _(
|
||||
'Signatures for document version: %s'
|
||||
) % self.get_document_version(),
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.get_document_version().signatures.all()
|
||||
|
||||
try:
|
||||
Permission.check_permissions(
|
||||
self.request.user, (permission_document_version_signature_view,)
|
||||
)
|
||||
except PermissionDenied:
|
||||
return AccessControlList.objects.filter_by_access(
|
||||
permission_document_version_signature_view, self.request.user, queryset
|
||||
)
|
||||
else:
|
||||
return queryset
|
||||
|
||||
|
||||
def document_verify(request, document_pk):
|
||||
document = get_object_or_404(Document, pk=document_pk)
|
||||
|
||||
@@ -84,19 +158,20 @@ def document_verify(request, document_pk):
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def document_signature_upload(request, document_pk):
|
||||
document = get_object_or_404(Document, pk=document_pk)
|
||||
|
||||
def document_version_signature_upload(request, pk):
|
||||
document_version = get_object_or_404(DocumentVersion, pk=pk)
|
||||
|
||||
try:
|
||||
Permission.check_permissions(
|
||||
request.user, (permission_signature_upload,)
|
||||
request.user, (permission_document_version_signature_upload,)
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(
|
||||
permission_signature_upload, request.user, document
|
||||
permission_document_version_signature_upload, request.user, document_version.document
|
||||
)
|
||||
|
||||
document.add_as_recent_document_for_user(request.user)
|
||||
document_version.document.add_as_recent_document_for_user(request.user)
|
||||
|
||||
post_action_redirect = None
|
||||
previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
@@ -106,8 +181,9 @@ def document_signature_upload(request, document_pk):
|
||||
form = DetachedSignatureForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
try:
|
||||
DocumentVersionSignature.objects.add_detached_signature(
|
||||
document.latest_version, request.FILES['file']
|
||||
DetachedSignature.objects.upload_signature(
|
||||
document_version=document_version,
|
||||
signature_file=request.FILES['file']
|
||||
)
|
||||
messages.success(
|
||||
request, _('Detached signature uploaded successfully.')
|
||||
@@ -122,9 +198,11 @@ def document_signature_upload(request, document_pk):
|
||||
return render_to_response('appearance/generic_form.html', {
|
||||
'form': form,
|
||||
'next': next,
|
||||
'object': document,
|
||||
'document': document_version.document,
|
||||
'document_version': document_version,
|
||||
'navigation_object_list': ('document', 'document_version'),
|
||||
'previous': previous,
|
||||
'title': _('Upload detached signature for document: %s') % document,
|
||||
'title': _('Upload detached signature for document version: %s') % document_version,
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
|
||||
@@ -133,11 +211,11 @@ def document_signature_download(request, document_pk):
|
||||
|
||||
try:
|
||||
Permission.check_permissions(
|
||||
request.user, (permission_signature_download,)
|
||||
request.user, (permission_document_version_signature_download,)
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(
|
||||
permission_signature_download, request.user, document
|
||||
permission_document_version_signature_download, request.user, document
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -156,49 +234,3 @@ def document_signature_download(request, document_pk):
|
||||
return HttpResponseRedirect(request.META['HTTP_REFERER'])
|
||||
|
||||
return HttpResponseRedirect(request.META['HTTP_REFERER'])
|
||||
|
||||
|
||||
def document_signature_delete(request, document_pk):
|
||||
document = get_object_or_404(Document, pk=document_pk)
|
||||
|
||||
try:
|
||||
Permission.check_permissions(
|
||||
request.user, (permission_signature_delete,)
|
||||
)
|
||||
except PermissionDenied:
|
||||
AccessControlList.objects.check_access(
|
||||
permission_signature_delete, request.user, document
|
||||
)
|
||||
|
||||
document.add_as_recent_document_for_user(request.user)
|
||||
|
||||
post_action_redirect = None
|
||||
previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', reverse(settings.LOGIN_REDIRECT_URL))))
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
DocumentVersionSignature.objects.clear_detached_signature(
|
||||
document.latest_version
|
||||
)
|
||||
messages.success(
|
||||
request, _('Detached signature deleted successfully.')
|
||||
)
|
||||
return HttpResponseRedirect(next)
|
||||
except Exception as exception:
|
||||
messages.error(
|
||||
request, _(
|
||||
'Error while deleting the detached signature; %s'
|
||||
) % exception
|
||||
)
|
||||
return HttpResponseRedirect(previous)
|
||||
|
||||
return render_to_response('appearance/generic_confirm.html', {
|
||||
'delete_view': True,
|
||||
'next': next,
|
||||
'object': document,
|
||||
'previous': previous,
|
||||
'title': _(
|
||||
'Delete the detached signature from document: %s?'
|
||||
) % document,
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
18
mayan/apps/documents/migrations/0033_auto_20160325_0052.py
Normal file
18
mayan/apps/documents/migrations/0033_auto_20160325_0052.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('documents', '0032_auto_20160315_0537'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='documenttypefilename',
|
||||
options={'ordering': ('filename',), 'verbose_name': 'Quick label', 'verbose_name_plural': 'Quick labels'},
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user