From ee6bb866c9df52eabb55f6dddc553c7b13ab2ed0 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 4 Jul 2015 02:25:59 -0400 Subject: [PATCH] Add support for document retention policies. Closes gh-issue #189. --- docs/releases/2.0.rst | 2 + mayan/apps/common/literals.py | 9 ++++ mayan/apps/documents/admin.py | 46 ++++++++++++------- mayan/apps/documents/apps.py | 20 ++++++++ mayan/apps/documents/forms.py | 2 +- mayan/apps/documents/literals.py | 4 ++ .../migrations/0011_auto_20150704_0508.py | 44 ++++++++++++++++++ mayan/apps/documents/models.py | 8 +++- mayan/apps/documents/tasks.py | 44 +++++++++++++++++- 9 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 mayan/apps/common/literals.py create mode 100644 mayan/apps/documents/migrations/0011_auto_20150704_0508.py diff --git a/docs/releases/2.0.rst b/docs/releases/2.0.rst index 2bdeba1af4..bb13290e47 100644 --- a/docs/releases/2.0.rst +++ b/docs/releases/2.0.rst @@ -61,6 +61,8 @@ What's new in Mayan EDMS v2.0 * Simplification of permissions/ACLS and role system * Removal of the ImageMagick and GraphicsMagick converter backends * 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 ================================= diff --git a/mayan/apps/common/literals.py b/mayan/apps/common/literals.py new file mode 100644 index 0000000000..e1d6e61813 --- /dev/null +++ b/mayan/apps/common/literals.py @@ -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')), +) diff --git a/mayan/apps/documents/admin.py b/mayan/apps/documents/admin.py index cbb5bd0ac3..d4fb53c826 100644 --- a/mayan/apps/documents/admin.py +++ b/mayan/apps/documents/admin.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.contrib import admin from .models import ( - Document, DocumentPage, DocumentType, DocumentTypeFilename, + DeletedDocument, Document, DocumentPage, DocumentType, DocumentTypeFilename, DocumentVersion, RecentDocument ) @@ -15,13 +15,6 @@ class DocumentPageInline(admin.StackedInline): allow_add = True -class DocumentVersionInline(admin.StackedInline): - model = DocumentVersion - extra = 1 - classes = ('collapse-open',) - allow_add = True - - class DocumentTypeFilenameInline(admin.StackedInline): model = DocumentTypeFilename extra = 1 @@ -29,27 +22,46 @@ class DocumentTypeFilenameInline(admin.StackedInline): allow_add = True -class DocumentTypeAdmin(admin.ModelAdmin): - inlines = [ - DocumentTypeFilenameInline - ] +class DocumentVersionInline(admin.StackedInline): + model = DocumentVersion + 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): + date_hierarchy = 'date_added' inlines = [ 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): - model = RecentDocument - list_display = ('user', 'document', 'datetime_accessed') - readonly_fields = ('user', 'document', 'datetime_accessed') - list_filter = ('user',) 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(DocumentType, DocumentTypeAdmin) admin.site.register(RecentDocument, RecentDocumentAdmin) diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index b24e0d3aed..fbf4df12b3 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -1,5 +1,7 @@ from __future__ import absolute_import, unicode_literals +from datetime import timedelta + from django.utils.translation import ugettext_lazy as _ from actstream import registry @@ -21,6 +23,7 @@ from converter.permissions import ( permission_transformation_view, ) from events.permissions import permission_events_view +from mayan.celery import app from navigation import SourceColumn from rest_api.classes import APIEndPoint from statistics.classes import StatisticNamespace @@ -51,6 +54,7 @@ from .links import ( link_document_version_download, link_document_version_list, link_document_version_revert, link_trash_can_empty ) +from .literals import CHECK_DELETE_PERIOD_INTERVAL, CHECK_TRASH_PERIOD_INTERVAL from .models import ( DeletedDocument, Document, DocumentPage, DocumentType, DocumentTypeFilename, DocumentVersion @@ -99,6 +103,22 @@ class DocumentsApp(MayanAppConfig): SourceColumn(source=DeletedDocument, label=_('Type'), attribute='document_type') 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_setup.bind_links(links=[link_document_type_setup]) menu_tools.bind_links(links=[link_clear_image_cache]) diff --git a/mayan/apps/documents/forms.py b/mayan/apps/documents/forms.py index 56307a84d2..f078d9ba84 100644 --- a/mayan/apps/documents/forms.py +++ b/mayan/apps/documents/forms.py @@ -100,7 +100,7 @@ class DocumentTypeForm(forms.ModelForm): Model class form to create or edit a document type """ class Meta: - fields = ('name',) + fields = ('name', 'trash_time_period', 'trash_time_unit', 'delete_time_period', 'delete_time_unit') model = DocumentType diff --git a/mayan/apps/documents/literals.py b/mayan/apps/documents/literals.py index 0d0de6b851..4cb49041ff 100644 --- a/mayan/apps/documents/literals.py +++ b/mayan/apps/documents/literals.py @@ -1,4 +1,8 @@ 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' DOCUMENT_IMAGE_TASK_TIMEOUT = 20 diff --git a/mayan/apps/documents/migrations/0011_auto_20150704_0508.py b/mayan/apps/documents/migrations/0011_auto_20150704_0508.py new file mode 100644 index 0000000000..905f8067ca --- /dev/null +++ b/mayan/apps/documents/migrations/0011_auto_20150704_0508.py @@ -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, + ), + ] diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py index dd21d4eed8..1fb25854cb 100644 --- a/mayan/apps/documents/models.py +++ b/mayan/apps/documents/models.py @@ -13,6 +13,7 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now 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.utils import fs_cleanup from converter import ( @@ -28,6 +29,7 @@ from .events import ( event_document_create, event_document_new_version, event_document_version_revert ) +from .literals import DEFAULT_DELETE_PERIOD, DEFAULT_DELETE_TIME_UNIT from .managers import ( DocumentManager, DocumentTypeManager, PassthroughManager, RecentDocumentManager, TrashCanManager @@ -53,7 +55,11 @@ class DocumentType(models.Model): Define document types or classes to which a specific set of 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() diff --git a/mayan/apps/documents/tasks.py b/mayan/apps/documents/tasks.py index 6f107e761d..117c1dde0b 100644 --- a/mayan/apps/documents/tasks.py +++ b/mayan/apps/documents/tasks.py @@ -1,15 +1,19 @@ from __future__ import unicode_literals +from datetime import timedelta import logging from django.contrib.auth.models import User from django.core.files import File +from django.utils.timezone import now from mayan.celery import app from common.models import SharedUploadedFile -from .models import Document, DocumentPage, DocumentType, DocumentVersion +from .models import ( + DeletedDocument, Document, DocumentPage, DocumentType, DocumentVersion +) 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) finally: 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')