Add support for document retention policies. Closes gh-issue #189.
This commit is contained in:
@@ -61,6 +61,8 @@ What's new in Mayan EDMS v2.0
|
|||||||
* Simplification of permissions/ACLS and role system
|
* Simplification of permissions/ACLS and role system
|
||||||
* Removal of the ImageMagick and GraphicsMagick converter backends
|
* Removal of the ImageMagick and GraphicsMagick converter backends
|
||||||
* Remove support for applying roles to new users automatically
|
* Remove support for applying roles to new users automatically
|
||||||
|
* Trash can feature
|
||||||
|
* Retention policies, auto move to trash, auto delete from trash
|
||||||
|
|
||||||
Upgrading from a previous version
|
Upgrading from a previous version
|
||||||
=================================
|
=================================
|
||||||
|
|||||||
9
mayan/apps/common/literals.py
Normal file
9
mayan/apps/common/literals.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
TIME_DELTA_UNIT_CHOICES = (
|
||||||
|
('days', _('Days')),
|
||||||
|
('hours', _('Hours')),
|
||||||
|
('minutes', _('Minutes')),
|
||||||
|
)
|
||||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Document, DocumentPage, DocumentType, DocumentTypeFilename,
|
DeletedDocument, Document, DocumentPage, DocumentType, DocumentTypeFilename,
|
||||||
DocumentVersion, RecentDocument
|
DocumentVersion, RecentDocument
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,13 +15,6 @@ class DocumentPageInline(admin.StackedInline):
|
|||||||
allow_add = True
|
allow_add = True
|
||||||
|
|
||||||
|
|
||||||
class DocumentVersionInline(admin.StackedInline):
|
|
||||||
model = DocumentVersion
|
|
||||||
extra = 1
|
|
||||||
classes = ('collapse-open',)
|
|
||||||
allow_add = True
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentTypeFilenameInline(admin.StackedInline):
|
class DocumentTypeFilenameInline(admin.StackedInline):
|
||||||
model = DocumentTypeFilename
|
model = DocumentTypeFilename
|
||||||
extra = 1
|
extra = 1
|
||||||
@@ -29,27 +22,46 @@ class DocumentTypeFilenameInline(admin.StackedInline):
|
|||||||
allow_add = True
|
allow_add = True
|
||||||
|
|
||||||
|
|
||||||
class DocumentTypeAdmin(admin.ModelAdmin):
|
class DocumentVersionInline(admin.StackedInline):
|
||||||
inlines = [
|
model = DocumentVersion
|
||||||
DocumentTypeFilenameInline
|
extra = 1
|
||||||
]
|
classes = ('collapse-open',)
|
||||||
|
allow_add = True
|
||||||
|
|
||||||
|
|
||||||
|
class DeletedDocumentAdmin(admin.ModelAdmin):
|
||||||
|
date_hierarchy = 'deleted_date_time'
|
||||||
|
list_filter = ('document_type',)
|
||||||
|
list_display = ('uuid', 'label', 'document_type', 'deleted_date_time')
|
||||||
|
readonly_fields = ('uuid', 'document_type')
|
||||||
|
|
||||||
|
|
||||||
class DocumentAdmin(admin.ModelAdmin):
|
class DocumentAdmin(admin.ModelAdmin):
|
||||||
|
date_hierarchy = 'date_added'
|
||||||
inlines = [
|
inlines = [
|
||||||
DocumentVersionInline
|
DocumentVersionInline
|
||||||
]
|
]
|
||||||
list_display = ('uuid', 'label',)
|
list_filter = ('document_type',)
|
||||||
|
list_display = ('uuid', 'label', 'document_type', 'date_added')
|
||||||
|
readonly_fields = ('uuid', 'document_type', 'date_added')
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentTypeAdmin(admin.ModelAdmin):
|
||||||
|
inlines = (
|
||||||
|
DocumentTypeFilenameInline,
|
||||||
|
)
|
||||||
|
list_display = ('name', 'trash_time_period', 'trash_time_unit', 'delete_time_period', 'delete_time_unit')
|
||||||
|
|
||||||
|
|
||||||
class RecentDocumentAdmin(admin.ModelAdmin):
|
class RecentDocumentAdmin(admin.ModelAdmin):
|
||||||
model = RecentDocument
|
|
||||||
list_display = ('user', 'document', 'datetime_accessed')
|
|
||||||
readonly_fields = ('user', 'document', 'datetime_accessed')
|
|
||||||
list_filter = ('user',)
|
|
||||||
date_hierarchy = 'datetime_accessed'
|
date_hierarchy = 'datetime_accessed'
|
||||||
|
list_display = ('user', 'document', 'datetime_accessed')
|
||||||
|
list_display_links = ('document', 'datetime_accessed')
|
||||||
|
list_filter = ('user',)
|
||||||
|
readonly_fields = ('user', 'document', 'datetime_accessed')
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(DeletedDocument, DeletedDocumentAdmin)
|
||||||
admin.site.register(Document, DocumentAdmin)
|
admin.site.register(Document, DocumentAdmin)
|
||||||
admin.site.register(DocumentType, DocumentTypeAdmin)
|
admin.site.register(DocumentType, DocumentTypeAdmin)
|
||||||
admin.site.register(RecentDocument, RecentDocumentAdmin)
|
admin.site.register(RecentDocument, RecentDocumentAdmin)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from actstream import registry
|
from actstream import registry
|
||||||
@@ -21,6 +23,7 @@ from converter.permissions import (
|
|||||||
permission_transformation_view,
|
permission_transformation_view,
|
||||||
)
|
)
|
||||||
from events.permissions import permission_events_view
|
from events.permissions import permission_events_view
|
||||||
|
from mayan.celery import app
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
from statistics.classes import StatisticNamespace
|
from statistics.classes import StatisticNamespace
|
||||||
@@ -51,6 +54,7 @@ from .links import (
|
|||||||
link_document_version_download, link_document_version_list,
|
link_document_version_download, link_document_version_list,
|
||||||
link_document_version_revert, link_trash_can_empty
|
link_document_version_revert, link_trash_can_empty
|
||||||
)
|
)
|
||||||
|
from .literals import CHECK_DELETE_PERIOD_INTERVAL, CHECK_TRASH_PERIOD_INTERVAL
|
||||||
from .models import (
|
from .models import (
|
||||||
DeletedDocument, Document, DocumentPage, DocumentType, DocumentTypeFilename,
|
DeletedDocument, Document, DocumentPage, DocumentType, DocumentTypeFilename,
|
||||||
DocumentVersion
|
DocumentVersion
|
||||||
@@ -99,6 +103,22 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
SourceColumn(source=DeletedDocument, label=_('Type'), attribute='document_type')
|
SourceColumn(source=DeletedDocument, label=_('Type'), attribute='document_type')
|
||||||
SourceColumn(source=DeletedDocument, label=_('Date time trashed'), attribute='deleted_date_time')
|
SourceColumn(source=DeletedDocument, label=_('Date time trashed'), attribute='deleted_date_time')
|
||||||
|
|
||||||
|
app.conf.CELERYBEAT_SCHEDULE.update({
|
||||||
|
'task_check_trash_periods': {
|
||||||
|
'task': 'documents.tasks.task_check_trash_periods',
|
||||||
|
'schedule': timedelta(seconds=CHECK_TRASH_PERIOD_INTERVAL),
|
||||||
|
'options': {'queue': 'documents'}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
app.conf.CELERYBEAT_SCHEDULE.update({
|
||||||
|
'task_check_delete_periods': {
|
||||||
|
'task': 'documents.tasks.task_check_delete_periods',
|
||||||
|
'schedule': timedelta(seconds=CHECK_DELETE_PERIOD_INTERVAL),
|
||||||
|
'options': {'queue': 'documents'}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
menu_front_page.bind_links(links=[link_document_list_recent, link_document_list, link_document_list_deleted])
|
menu_front_page.bind_links(links=[link_document_list_recent, link_document_list, link_document_list_deleted])
|
||||||
menu_setup.bind_links(links=[link_document_type_setup])
|
menu_setup.bind_links(links=[link_document_type_setup])
|
||||||
menu_tools.bind_links(links=[link_clear_image_cache])
|
menu_tools.bind_links(links=[link_clear_image_cache])
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class DocumentTypeForm(forms.ModelForm):
|
|||||||
Model class form to create or edit a document type
|
Model class form to create or edit a document type
|
||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('name',)
|
fields = ('name', 'trash_time_period', 'trash_time_unit', 'delete_time_period', 'delete_time_unit')
|
||||||
model = DocumentType
|
model = DocumentType
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
CHECK_DELETE_PERIOD_INTERVAL = 60
|
||||||
|
CHECK_TRASH_PERIOD_INTERVAL = 60
|
||||||
|
DEFAULT_DELETE_PERIOD = 30
|
||||||
|
DEFAULT_DELETE_TIME_UNIT = 'days'
|
||||||
DEFAULT_ZIP_FILENAME = 'document_bundle.zip'
|
DEFAULT_ZIP_FILENAME = 'document_bundle.zip'
|
||||||
DOCUMENT_IMAGE_TASK_TIMEOUT = 20
|
DOCUMENT_IMAGE_TASK_TIMEOUT = 20
|
||||||
|
|||||||
44
mayan/apps/documents/migrations/0011_auto_20150704_0508.py
Normal file
44
mayan/apps/documents/migrations/0011_auto_20150704_0508.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('documents', '0010_auto_20150704_0054'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='documenttype',
|
||||||
|
name='delete_time_period',
|
||||||
|
field=models.PositiveIntegerField(default=30, verbose_name='Delete time period'),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='documenttype',
|
||||||
|
name='delete_time_unit',
|
||||||
|
field=models.CharField(default='days', max_length=8, verbose_name='Delete time unit', choices=[('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes'), ('seconds', 'Seconds')]),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='documenttype',
|
||||||
|
name='trash_time_period',
|
||||||
|
field=models.PositiveIntegerField(null=True, verbose_name='Trash time period', blank=True),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='documenttype',
|
||||||
|
name='trash_time_unit',
|
||||||
|
field=models.CharField(blank=True, max_length=8, null=True, verbose_name='Trash time unit', choices=[('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes'), ('seconds', 'Seconds')]),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='deleted_date_time',
|
||||||
|
field=models.DateTimeField(verbose_name='Date and time trashed', blank=True),
|
||||||
|
preserve_default=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -13,6 +13,7 @@ from django.utils.encoding import python_2_unicode_compatible
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from common.literals import TIME_DELTA_UNIT_CHOICES
|
||||||
from common.settings import setting_temporary_directory
|
from common.settings import setting_temporary_directory
|
||||||
from common.utils import fs_cleanup
|
from common.utils import fs_cleanup
|
||||||
from converter import (
|
from converter import (
|
||||||
@@ -28,6 +29,7 @@ from .events import (
|
|||||||
event_document_create, event_document_new_version,
|
event_document_create, event_document_new_version,
|
||||||
event_document_version_revert
|
event_document_version_revert
|
||||||
)
|
)
|
||||||
|
from .literals import DEFAULT_DELETE_PERIOD, DEFAULT_DELETE_TIME_UNIT
|
||||||
from .managers import (
|
from .managers import (
|
||||||
DocumentManager, DocumentTypeManager, PassthroughManager,
|
DocumentManager, DocumentTypeManager, PassthroughManager,
|
||||||
RecentDocumentManager, TrashCanManager
|
RecentDocumentManager, TrashCanManager
|
||||||
@@ -53,7 +55,11 @@ class DocumentType(models.Model):
|
|||||||
Define document types or classes to which a specific set of
|
Define document types or classes to which a specific set of
|
||||||
properties can be attached
|
properties can be attached
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=32, verbose_name=_('Name'), unique=True)
|
name = models.CharField(max_length=32, unique=True, verbose_name=_('Name'))
|
||||||
|
trash_time_period = models.PositiveIntegerField(blank=True, help_text=_('Amount of time after which documents of this type will be moved to the trash.'), null=True, verbose_name=_('Trash time period'))
|
||||||
|
trash_time_unit = models.CharField(blank=True, choices=TIME_DELTA_UNIT_CHOICES, null=True, max_length=8, verbose_name=_('Trash time unit'))
|
||||||
|
delete_time_period = models.PositiveIntegerField(default=DEFAULT_DELETE_PERIOD, help_text=_('Amount of time after which documents of this type in the trash will be deleted.'), verbose_name=_('Delete time period'))
|
||||||
|
delete_time_unit = models.CharField(choices=TIME_DELTA_UNIT_CHOICES, default=DEFAULT_DELETE_TIME_UNIT, max_length=8, verbose_name=_('Delete time unit'))
|
||||||
|
|
||||||
objects = DocumentTypeManager()
|
objects = DocumentTypeManager()
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from mayan.celery import app
|
from mayan.celery import app
|
||||||
|
|
||||||
from common.models import SharedUploadedFile
|
from common.models import SharedUploadedFile
|
||||||
|
|
||||||
from .models import Document, DocumentPage, DocumentType, DocumentVersion
|
from .models import (
|
||||||
|
DeletedDocument, Document, DocumentPage, DocumentType, DocumentVersion
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -77,3 +81,41 @@ def task_upload_new_version(document_id, shared_uploaded_file_id, user_id, comme
|
|||||||
logger.info('Warning during attempt to create new document version for document:%s ; %s', document, warning)
|
logger.info('Warning during attempt to create new document version for document:%s ; %s', document, warning)
|
||||||
finally:
|
finally:
|
||||||
shared_file.delete()
|
shared_file.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(ignore_result=True)
|
||||||
|
def task_check_trash_periods():
|
||||||
|
logger.info('Executing')
|
||||||
|
|
||||||
|
for document_type in DocumentType.objects.all():
|
||||||
|
logger.info('Checking trash period of document type: %s', document_type)
|
||||||
|
if document_type.trash_time_period and document_type.trash_time_unit:
|
||||||
|
delta = timedelta(**{document_type.trash_time_unit: document_type.trash_time_period})
|
||||||
|
logger.info('Document type: %s, has a trash period delta of: %s', document_type, delta)
|
||||||
|
for document in Document.objects.filter(document_type=document_type):
|
||||||
|
if now() > document.date_added + delta:
|
||||||
|
logger.info('Document "%s" with id: %d, added on: %s, exceded trash period', document, document.pk, document.date_added)
|
||||||
|
document.delete()
|
||||||
|
else:
|
||||||
|
logger.info('Document type: %s, has a no retention delta', document_type)
|
||||||
|
|
||||||
|
logger.info('Finshed')
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(ignore_result=True)
|
||||||
|
def task_check_delete_periods():
|
||||||
|
logger.info('Executing')
|
||||||
|
|
||||||
|
for document_type in DocumentType.objects.all():
|
||||||
|
logger.info('Checking deletion period of document type: %s', document_type)
|
||||||
|
if document_type.delete_time_period and document_type.delete_time_unit:
|
||||||
|
delta = timedelta(**{document_type.delete_time_unit: document_type.delete_time_period})
|
||||||
|
logger.info('Document type: %s, has a deletion period delta of: %s', document_type, delta)
|
||||||
|
for document in DeletedDocument.objects.filter(document_type=document_type):
|
||||||
|
if now() > document.deleted_date_time + delta:
|
||||||
|
logger.info('Document "%s" with id: %d, trashed on: %s, exceded delete period', document, document.pk, document.deleted_date_time)
|
||||||
|
document.delete()
|
||||||
|
else:
|
||||||
|
logger.info('Document type: %s, has a no retention delta', document_type)
|
||||||
|
|
||||||
|
logger.info('Finshed')
|
||||||
|
|||||||
Reference in New Issue
Block a user