Add support for document retention policies. Closes gh-issue #189.

This commit is contained in:
Roberto Rosario
2015-07-04 02:25:59 -04:00
parent 589874bec1
commit ee6bb866c9
9 changed files with 159 additions and 20 deletions

View File

@@ -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
================================= =================================

View 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')),
)

View File

@@ -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)

View File

@@ -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])

View File

@@ -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

View File

@@ -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

View 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,
),
]

View File

@@ -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()

View File

@@ -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')