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
|
||||
* 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
|
||||
=================================
|
||||
|
||||
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 .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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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.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()
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user