Update the file caching app

- Add view to list available caches.
- Add links to view and purge caches.
- Add permissions.
- Add events.
- Add purge task.
- Remove document image clear link and view.
  This is now handled by the file caching app.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-07-25 02:22:57 -04:00
parent 9315776926
commit 88bc29e4d7
19 changed files with 309 additions and 56 deletions

View File

@@ -61,6 +61,8 @@
regardless of MIME type. regardless of MIME type.
- Remove task inspection from task manager app. - Remove task inspection from task manager app.
- Move pagination navigation inside the toolbar. - Move pagination navigation inside the toolbar.
- Remove document image clear link and view.
This is now handled by the file caching app.
3.2.6 (2019-07-10) 3.2.6 (2019-07-10)
================== ==================

View File

@@ -76,6 +76,8 @@ Changes
- Add platform template to return queues for a worker. - Add platform template to return queues for a worker.
- Remove task inspection from task manager app. - Remove task inspection from task manager app.
- Move pagination navigation inside the toolbar. - Move pagination navigation inside the toolbar.
- Remove document image clear link and view.
This is now handled by the file caching app.
Removals Removals

View File

@@ -47,7 +47,7 @@ from .handlers import (
handler_remove_empty_duplicates_lists, handler_scan_duplicates_for handler_remove_empty_duplicates_lists, handler_scan_duplicates_for
) )
from .links import ( from .links import (
link_clear_image_cache, link_document_clear_transformations, link_document_clear_transformations,
link_document_clone_transformations, link_document_delete, link_document_clone_transformations, link_document_delete,
link_document_document_type_edit, link_document_download, link_document_document_type_edit, link_document_download,
link_document_duplicates_list, link_document_edit, link_document_duplicates_list, link_document_edit,
@@ -377,7 +377,7 @@ class DocumentsApp(MayanAppConfig):
menu_setup.bind_links(links=(link_document_type_setup,)) menu_setup.bind_links(links=(link_document_type_setup,))
menu_tools.bind_links( menu_tools.bind_links(
links=(link_clear_image_cache, link_duplicated_document_scan) links=(link_duplicated_document_scan,)
) )
# Document type links # Document type links

View File

@@ -12,8 +12,6 @@ icon_document_type = Icon(
icon_menu_documents = Icon(driver_name='fontawesome', symbol='book') icon_menu_documents = Icon(driver_name='fontawesome', symbol='book')
icon_clear_image_cache = Icon(driver_name='fontawesome', symbol='file-image')
icon_dashboard_document_types = icon_document_type icon_dashboard_document_types = icon_document_type
icon_dashboard_documents_in_trash = Icon( icon_dashboard_documents_in_trash = Icon(
driver_name='fontawesome', symbol='trash-alt' driver_name='fontawesome', symbol='trash-alt'

View File

@@ -8,7 +8,7 @@ from mayan.apps.converter.permissions import (
from mayan.apps.navigation.classes import Link from mayan.apps.navigation.classes import Link
from .icons import ( from .icons import (
icon_clear_image_cache, icon_document_list_recent_access, icon_document_list_recent_access,
icon_recent_added_document_list, icon_document_page_navigation_first, icon_recent_added_document_list, icon_document_page_navigation_first,
icon_document_page_navigation_last, icon_document_page_navigation_next, icon_document_page_navigation_last, icon_document_page_navigation_next,
icon_document_page_navigation_previous, icon_document_page_return, icon_document_page_navigation_previous, icon_document_page_return,
@@ -264,16 +264,6 @@ link_document_list_deleted = Link(
text=_('Trash can'), view='documents:document_list_deleted' text=_('Trash can'), view='documents:document_list_deleted'
) )
# Tools
link_clear_image_cache = Link(
icon_class=icon_clear_image_cache,
description=_(
'Clear the graphics representations used to speed up the documents\' '
'display and interactive transformations results.'
), permissions=(permission_document_tools,),
text=_('Clear document image cache'),
view='documents:document_clear_image_cache'
)
link_trash_can_empty = Link( link_trash_can_empty = Link(
permissions=(permission_empty_trash,), text=_('Empty trash'), permissions=(permission_empty_trash,), text=_('Empty trash'),
view='documents:trash_can_empty' view='documents:trash_can_empty'

View File

@@ -61,10 +61,6 @@ queue_documents_periodic.add_task_type(
schedule=timedelta(seconds=DELETE_STALE_STUBS_INTERVAL), schedule=timedelta(seconds=DELETE_STALE_STUBS_INTERVAL),
) )
queue_tools.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_clear_image_cache',
label=_('Clear image cache')
)
queue_tools.add_task_type( queue_tools.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_all', dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_all',
label=_('Duplicated document scan') label=_('Duplicated document scan')

View File

@@ -41,17 +41,6 @@ def task_check_trash_periods():
DocumentType.objects.check_trash_periods() DocumentType.objects.check_trash_periods()
@app.task(ignore_result=True)
def task_clear_image_cache():
Document = apps.get_model(
app_label='documents', model_name='Document'
)
logger.info('Starting document cache invalidation')
Document.objects.invalidate_cache()
logger.info('Finished document cache invalidation')
@app.task(ignore_result=True) @app.task(ignore_result=True)
def task_delete_document(trashed_document_id): def task_delete_document(trashed_document_id):
DeletedDocument = apps.get_model( DeletedDocument = apps.get_model(

View File

@@ -13,7 +13,7 @@ from .api_views import (
APIRecentDocumentListView APIRecentDocumentListView
) )
from .views import ( from .views import (
ClearImageCacheView, DocumentDocumentTypeEditView, DocumentDownloadFormView, DocumentDocumentTypeEditView, DocumentDownloadFormView,
DocumentDownloadView, DocumentDuplicatesListView, DocumentEditView, DocumentDownloadView, DocumentDuplicatesListView, DocumentEditView,
DocumentListView, DocumentPageListView, DocumentPageNavigationFirst, DocumentListView, DocumentPageListView, DocumentPageNavigationFirst,
DocumentPageNavigationLast, DocumentPageNavigationNext, DocumentPageNavigationLast, DocumentPageNavigationNext,
@@ -272,10 +272,6 @@ urlpatterns = [
view=DocumentTransformationsClearView.as_view(), view=DocumentTransformationsClearView.as_view(),
name='document_multiple_clear_transformations' name='document_multiple_clear_transformations'
), ),
url(
regex=r'^cache/clear/$', view=ClearImageCacheView.as_view(),
name='document_clear_image_cache'
),
url( url(
regex=r'^page/(?P<pk>\d+)/$', view=DocumentPageView.as_view(), regex=r'^page/(?P<pk>\d+)/$', view=DocumentPageView.as_view(),
name='document_page_view' name='document_page_view'

View File

@@ -8,25 +8,12 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import ConfirmView from mayan.apps.common.generics import ConfirmView
from ..permissions import permission_document_tools from ..permissions import permission_document_tools
from ..tasks import task_clear_image_cache, task_scan_duplicates_all from ..tasks import task_scan_duplicates_all
__all__ = ('ClearImageCacheView', 'ScanDuplicatedDocuments') __all__ = ('ScanDuplicatedDocuments',)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ClearImageCacheView(ConfirmView):
extra_context = {
'title': _('Clear the document image cache?')
}
view_permission = permission_document_tools
def view_action(self):
task_clear_image_cache.apply_async()
messages.success(
self.request, _('Document cache clearing queued successfully.')
)
class ScanDuplicatedDocuments(ConfirmView): class ScanDuplicatedDocuments(ConfirmView):
extra_context = { extra_context = {
'title': _('Scan for duplicated documents?') 'title': _('Scan for duplicated documents?')

View File

@@ -1,8 +1,79 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.acls.links import link_acl_list
from mayan.apps.acls.permissions import permission_acl_edit, permission_acl_view
from mayan.apps.common.apps import MayanAppConfig from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.menus import (
menu_list_facet, menu_multi_item, menu_object, menu_secondary, menu_tools
)
from mayan.apps.events.classes import ModelEventType
from mayan.apps.events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list
)
from mayan.apps.navigation.classes import SourceColumn
from .events import event_cache_edited, event_cache_purged
from .links import (
link_caches_list, link_cache_multiple_purge, link_cache_purge
)
from .permissions import permission_cache_purge, permission_cache_view
class FileCachingConfig(MayanAppConfig): class FileCachingConfig(MayanAppConfig):
app_namespace = 'file_caching'
app_url = 'file_caching'
has_tests = False has_tests = False
name = 'mayan.apps.file_caching' name = 'mayan.apps.file_caching'
verbose_name = _('File caching')
def ready(self):
super(FileCachingConfig, self).ready()
from actstream import registry
Cache = self.get_model(model_name='Cache')
ModelEventType.register(
event_types=(event_cache_edited, event_cache_purged,),
model=Cache
)
ModelPermission.register(
model=Cache, permissions=(
permission_acl_edit, permission_acl_view,
permission_cache_purge, permission_cache_view
)
)
SourceColumn(attribute='name', source=Cache)
SourceColumn(attribute='label', source=Cache)
SourceColumn(attribute='storage_instance_path', source=Cache)
SourceColumn(attribute='get_maximum_size_display', source=Cache)
SourceColumn(attribute='get_total_size_display', source=Cache)
menu_list_facet.bind_links(
links=(
link_acl_list, link_events_for_object,
link_object_event_types_user_subcriptions_list,
), sources=(Cache,)
)
menu_object.bind_links(
links=(link_cache_purge,),
sources=(Cache,)
)
menu_multi_item.bind_links(
links=(link_cache_multiple_purge,),
sources=(Cache,)
)
menu_secondary.bind_links(
links=(link_caches_list,), sources=(
Cache,
)
)
menu_tools.bind_links(links=(link_caches_list,))
registry.register(Cache)

View File

@@ -0,0 +1,19 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.events.classes import EventTypeNamespace
namespace = EventTypeNamespace(
label=_('File caching'), name='file_caching'
)
event_cache_created = namespace.add_event_type(
label=_('Cache created'), name='cache_created'
)
event_cache_edited = namespace.add_event_type(
label=_('Cache edited'), name='cache_edited'
)
event_cache_purged = namespace.add_event_type(
label=_('Cache purge'), name='cache_purged'
)

View File

@@ -0,0 +1,11 @@
from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_file_caching = Icon(
driver_name='fontawesome', symbol='warehouse'
)
icon_cache_purge = Icon(
driver_name='fontawesome-dual', primary_symbol='warehouse',
secondary_symbol='check'
)

View File

@@ -0,0 +1,22 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.navigation.classes import Link
from .icons import icon_cache_purge, icon_file_caching
from .permissions import permission_cache_purge, permission_cache_view
link_caches_list = Link(
icon_class=icon_file_caching, permissions=(permission_cache_view,),
text=_('File caches'), view='file_caching:cache_list'
)
link_cache_purge = Link(
icon_class=icon_cache_purge, kwargs={'cache_id': 'resolved_object.id'},
permissions=(permission_cache_purge,), text=_('Purge cache'),
view='file_caching:cache_purge'
)
link_cache_multiple_purge = Link(
icon_class=icon_cache_purge, text=_('Purge cache'),
view='file_caching:cache_multiple_purge'
)

View File

@@ -6,6 +6,7 @@ import logging
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Sum from django.db.models import Sum
from django.template.defaultfilters import filesizeformat
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
@@ -14,18 +15,31 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.lock_manager.exceptions import LockError from mayan.apps.lock_manager.exceptions import LockError
from mayan.apps.lock_manager.runtime import locking_backend from mayan.apps.lock_manager.runtime import locking_backend
from .events import (
event_cache_created, event_cache_edited, event_cache_purged
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@python_2_unicode_compatible @python_2_unicode_compatible
class Cache(models.Model): class Cache(models.Model):
name = models.CharField( name = models.CharField(
max_length=128, unique=True, verbose_name=_('Name') help_text=_('Internal name of the cache.'), max_length=128,
unique=True, verbose_name=_('Name')
)
label = models.CharField(
help_text=_('A short text describing the cache.'), max_length=128,
verbose_name=_('Label')
)
maximum_size = models.PositiveIntegerField(
help_text=_('Maximum size of the cache in bytes.'),
verbose_name=_('Maximum size')
) )
label = models.CharField(max_length=128, verbose_name=_('Label'))
maximum_size = models.PositiveIntegerField(verbose_name=_('Maximum size'))
storage_instance_path = models.CharField( storage_instance_path = models.CharField(
max_length=255, unique=True, verbose_name=_('Storage instance path') help_text=_(
'Dotted path to the actual storage class used for the cache.'
), max_length=255, unique=True, verbose_name=_('Storage instance path')
) )
class Meta: class Meta:
@@ -38,21 +52,55 @@ class Cache(models.Model):
def get_files(self): def get_files(self):
return CachePartitionFile.objects.filter(partition__cache__id=self.pk) return CachePartitionFile.objects.filter(partition__cache__id=self.pk)
def get_maximum_size_display(self):
return filesizeformat(bytes_=self.maximum_size)
get_maximum_size_display.short_description = _('Maximum size')
def get_total_size(self): def get_total_size(self):
"""
Return the actual usage of the cache.
"""
return self.get_files().aggregate( return self.get_files().aggregate(
file_size__sum=Sum('file_size') file_size__sum=Sum('file_size')
)['file_size__sum'] or 0 )['file_size__sum'] or 0
def get_total_size_display(self):
return filesizeformat(bytes_=self.get_total_size())
get_total_size_display.short_description = _('Total size')
def prune(self): def prune(self):
"""
Deletes files until the total size of the cache is below the allowed
maximum size of the cache.
"""
while self.get_total_size() > self.maximum_size: while self.get_total_size() > self.maximum_size:
self.get_files().earliest().delete() self.get_files().earliest().delete()
def purge(self): def purge(self, _user=None):
"""
Deletes the entire cache.
"""
for partition in self.partitions.all(): for partition in self.partitions.all():
partition.purge() partition.purge()
event_cache_purged.commit(actor=_user, target=self)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
_user = kwargs.pop('_user', None)
with transaction.atomic():
is_new = not self.pk
result = super(Cache, self).save(*args, **kwargs) result = super(Cache, self).save(*args, **kwargs)
if is_new:
event_cache_created.commit(
actor=_user, target=self
)
else:
event_cache_edited.commit(
actor=_user, target=self
)
self.prune() self.prune()
return result return result

View File

@@ -0,0 +1,14 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('File caching'), name='file_caching')
permission_cache_purge = namespace.add_permission(
label=_('Purge a file cache'), name='file_caching_cache_purge'
)
permission_cache_view = namespace.add_permission(
label=_('View a file cache'), name='file_caching_cache_view'
)

View File

@@ -0,0 +1,10 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.queues import queue_tools
queue_tools.add_task_type(
dotted_path='mayan.apps.file_caching.tasks.task_cache_purge',
label=_('Purge a file cache')
)

View File

@@ -0,0 +1,25 @@
from __future__ import unicode_literals
import logging
from django.apps import apps
from django.contrib.auth import get_user_model
from mayan.celery import app
logger = logging.getLogger(__name__)
@app.task(ignore_result=True)
def task_cache_purge(cache_id, user_id=None):
Cache = apps.get_model(
app_label='file_caching', model_name='Cache'
)
User = get_user_model()
cache = Cache.objects.get(pk=cache_id)
user = User.objects.get(pk=user_id)
logger.info('Starting cache id %s purge', cache)
cache.purge(_user=user)
logger.info('Finished cache id %s purge', cache)

View File

@@ -0,0 +1,20 @@
from __future__ import unicode_literals
from django.conf.urls import url
from .views import CacheListView, CachePurgeView
urlpatterns = [
url(
regex=r'^caches/$',
name='cache_list', view=CacheListView.as_view()
),
url(
regex=r'^caches/(?P<cache_id>\d+)/purge/$',
name='cache_purge', view=CachePurgeView.as_view()
),
url(
regex=r'^caches/multiple/purge/$', name='cache_multiple_purge',
view=CachePurgeView.as_view()
),
]

View File

@@ -0,0 +1,53 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
from mayan.apps.common.generics import (
MultipleObjectConfirmActionView, SingleObjectListView
)
from .models import Cache
from .permissions import permission_cache_purge, permission_cache_view
from .tasks import task_cache_purge
class CacheListView(SingleObjectListView):
model = Cache
permission = permission_cache_view
def get_extra_context(self):
return {
'hide_object': True,
'title': _('File caches list')
}
class CachePurgeView(MultipleObjectConfirmActionView):
model = Cache
object_permission = permission_cache_purge
pk_url_kwarg = 'cache_id'
success_message_singular = '%(count)d cache submitted for purging.'
success_message_plural = '%(count)d caches submitted for purging.'
def get_extra_context(self):
queryset = self.object_list
result = {
'title': ungettext(
singular='Submit the selected cache for purging?',
plural='Submit the selected caches for purging?',
number=queryset.count()
)
}
if queryset.count() == 1:
result['object'] = queryset.first()
return result
def object_action(self, form, instance):
task_cache_purge.apply_async(
kwargs={'cache_id': instance.pk, 'user_id': self.request.user.pk}
)