Backport ACL computation improvements

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2019-05-04 03:27:30 -04:00
parent d271f1503d
commit 8e731d6280
61 changed files with 701 additions and 609 deletions

View File

@@ -16,7 +16,7 @@ from mayan.apps.rest_api.permissions import MayanPermission
from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT
from .models import (
Document, DocumentType, RecentDocument
DeletedDocument, Document, DocumentType, RecentDocument
)
from .permissions import (
permission_document_create, permission_document_delete,
@@ -42,14 +42,14 @@ from .tasks import task_generate_document_page_image
logger = logging.getLogger(__name__)
class APIDeletedDocumentListView(generics.ListAPIView):
class APITrashedDocumentListView(generics.ListAPIView):
"""
Returns a list of all the trashed documents.
"""
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {'GET': (permission_document_view,)}
permission_classes = (MayanPermission,)
queryset = Document.trash.all()
queryset = DeletedDocument.objects.all()
serializer_class = DeletedDocumentSerializer
@@ -64,7 +64,7 @@ class APIDeletedDocumentView(generics.RetrieveDestroyAPIView):
'GET': (permission_document_view,)
}
permission_classes = (MayanPermission,)
queryset = Document.trash.all()
queryset = DeletedDocument.objects.all()
serializer_class = DeletedDocumentSerializer
@@ -76,7 +76,7 @@ class APIDeletedDocumentRestoreView(generics.GenericAPIView):
'POST': (permission_document_restore,)
}
permission_classes = (MayanPermission,)
queryset = Document.trash.all()
queryset = DeletedDocument.objects.all()
def get_serializer(self, *args, **kwargs):
return None
@@ -143,8 +143,8 @@ class APIDocumentListView(generics.ListCreateAPIView):
def perform_create(self, serializer):
AccessControlList.objects.check_access(
permissions=(permission_document_create,), user=self.request.user,
obj=serializer.validated_data['document_type']
obj=serializer.validated_data['document_type'],
permissions=(permission_document_create,), user=self.request.user
)
serializer.save(_user=self.request.user)
@@ -164,7 +164,8 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
document = get_object_or_404(Document.passthrough, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permission_required, self.request.user, document
obj=document, permissions=(permission_required,),
user=self.request.user
)
return document
@@ -231,7 +232,8 @@ class APIDocumentPageView(generics.RetrieveUpdateAPIView):
document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permission_required, self.request.user, document
obj=document, permissions=(permission_required,),
user=self.request.user
)
return document
@@ -309,8 +311,8 @@ class APIDocumentTypeDocumentListView(generics.ListAPIView):
def get_queryset(self):
document_type = get_object_or_404(DocumentType, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_document_type_view, user=self.request.user,
obj=document_type
obj=document_type, permissions=(permission_document_type_view,),
user=self.request.user
)
return document_type.documents.all()
@@ -326,8 +328,8 @@ class APIDocumentVersionDownloadView(DownloadMixin, generics.RetrieveAPIView):
document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=(permission_document_download,), user=self.request.user,
obj=document
obj=document, permissions=(permission_document_download,),
user=self.request.user
)
return document
@@ -420,7 +422,8 @@ class APIDocumentVersionPageListView(generics.ListAPIView):
document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permission_document_view, self.request.user, document
obj=document, permissions=(permission_document_view,),
user=self.request.user
)
return document
@@ -442,7 +445,6 @@ class APIDocumentVersionsListView(generics.ListCreateAPIView):
mayan_object_permissions = {
'GET': (permission_document_version_view,),
}
mayan_permission_attribute_check = 'document'
permission_classes = (MayanPermission,)
def create(self, request, *args, **kwargs):
@@ -471,8 +473,8 @@ class APIDocumentVersionsListView(generics.ListCreateAPIView):
document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=(permission_document_new_version,),
user=self.request.user, obj=document
obj=document, permissions=(permission_document_new_version,),
user=self.request.user,
)
serializer.save(document=document, _user=self.request.user)
@@ -497,7 +499,8 @@ class APIDocumentVersionView(generics.RetrieveUpdateDestroyAPIView):
document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permission_required, self.request.user, document
obj=document, permissions=(permission_required,),
user=self.request.user
)
return document

View File

@@ -220,10 +220,6 @@ class DocumentsApp(MayanAppConfig):
)
)
ModelPermission.register_proxy(
source=Document, model=DocumentType,
)
ModelPermission.register_inheritance(
model=Document, related='document_type',
)

View File

@@ -53,13 +53,13 @@ def task_clear_image_cache():
@app.task(ignore_result=True)
def task_delete_document(deleted_document_id):
def task_delete_document(trashed_document_id):
DeletedDocument = apps.get_model(
app_label='documents', model_name='DeletedDocument'
)
logger.debug(msg='Executing')
deleted_document = DeletedDocument.objects.get(pk=deleted_document_id)
deleted_document = DeletedDocument.objects.get(pk=trashed_document_id)
deleted_document.delete()
logger.debug(msg='Finshed')

View File

@@ -9,7 +9,7 @@ from ..permissions import (
from .base import GenericDocumentViewTestCase
class DeletedDocumentTestCase(GenericDocumentViewTestCase):
class TrashedDocumentTestCase(GenericDocumentViewTestCase):
def _request_document_restore_view(self):
return self.post(
viewname='documents:document_restore', kwargs={
@@ -22,7 +22,7 @@ class DeletedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(Document.objects.count(), 0)
response = self._request_document_restore_view()
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 302)
self.assertEqual(DeletedDocument.objects.count(), 1)
self.assertEqual(Document.objects.count(), 0)
@@ -50,7 +50,7 @@ class DeletedDocumentTestCase(GenericDocumentViewTestCase):
def test_document_trash_no_permissions(self):
response = self._request_document_trash_view()
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 302)
self.assertEqual(DeletedDocument.objects.count(), 0)
self.assertEqual(Document.objects.count(), 1)
@@ -79,7 +79,7 @@ class DeletedDocumentTestCase(GenericDocumentViewTestCase):
self.assertEqual(DeletedDocument.objects.count(), 1)
response = self._request_document_delete_view()
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 302)
self.assertEqual(Document.objects.count(), 0)
self.assertEqual(DeletedDocument.objects.count(), 1)

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .api_views import (
APIDeletedDocumentListView, APIDeletedDocumentRestoreView,
APITrashedDocumentListView, APIDeletedDocumentRestoreView,
APIDeletedDocumentView, APIDocumentDownloadView, APIDocumentView,
APIDocumentListView, APIDocumentVersionDownloadView,
APIDocumentPageImageView, APIDocumentPageView,
@@ -13,31 +13,33 @@ from .api_views import (
APIRecentDocumentListView
)
from .views import (
ClearImageCacheView, DeletedDocumentDeleteView,
DeletedDocumentDeleteManyView, DeletedDocumentListView,
DocumentDocumentTypeEditView, DocumentDownloadFormView,
ClearImageCacheView, DocumentDocumentTypeEditView, DocumentDownloadFormView,
DocumentDownloadView, DocumentDuplicatesListView, DocumentEditView,
DocumentListView, DocumentPageListView, DocumentPageNavigationFirst,
DocumentPageNavigationLast, DocumentPageNavigationNext,
DocumentPageNavigationPrevious, DocumentPageRotateLeftView,
DocumentPageRotateRightView, DocumentPageView, DocumentPageViewResetView,
DocumentPageZoomInView, DocumentPageZoomOutView, DocumentPreviewView,
DocumentPrint, DocumentRestoreView, DocumentRestoreManyView,
DocumentTransformationsClearView, DocumentTransformationsCloneView,
DocumentTrashView, DocumentTrashManyView, DocumentTypeCreateView,
DocumentPrint, DocumentTransformationsClearView,
DocumentTransformationsCloneView, DocumentTypeCreateView,
DocumentTypeDeleteView, DocumentTypeDocumentListView,
DocumentTypeFilenameCreateView, DocumentTypeFilenameDeleteView,
DocumentTypeFilenameEditView, DocumentTypeFilenameListView,
DocumentTypeListView, DocumentTypeEditView, DocumentUpdatePageCountView,
DocumentVersionDownloadFormView, DocumentVersionDownloadView,
DocumentVersionListView, DocumentVersionRevertView, DocumentVersionView,
DocumentView, DuplicatedDocumentListView, EmptyTrashCanView,
DocumentView, DuplicatedDocumentListView,
RecentAccessDocumentListView, RecentAddedDocumentListView,
ScanDuplicatedDocuments
)
from .views.favorite_document_views import (
FavoriteAddView, FavoriteDocumentListView, FavoriteRemoveView
)
from .views.trashed_document_views import (
DocumentTrashView, EmptyTrashCanView, TrashedDocumentDeleteView,
TrashedDocumentListView, TrashedDocumentRestoreView
)
urlpatterns_favorite_documents = [
url(
@@ -62,6 +64,42 @@ urlpatterns_favorite_documents = [
view=FavoriteRemoveView.as_view(),
name='document_multiple_remove_from_favorites'
),
url(
regex=r'^trash_can/empty/$', view=EmptyTrashCanView.as_view(),
name='trash_can_empty'
),
]
urlpatterns_trashed_documents = [
url(
regex=r'^(?P<pk>\d+)/trash/$', view=DocumentTrashView.as_view(),
name='document_trash'
),
url(
regex=r'^multiple/trash/$', view=DocumentTrashView.as_view(),
name='document_multiple_trash'
),
url(
regex=r'^list/deleted/$', view=TrashedDocumentListView.as_view(),
name='document_list_deleted'
),
url(
regex=r'^(?P<pk>\d+)/restore/$',
view=TrashedDocumentRestoreView.as_view(), name='document_restore'
),
url(
regex=r'^multiple/restore/$', view=TrashedDocumentRestoreView.as_view(),
name='document_multiple_restore'
),
url(
regex=r'^(?P<pk>\d+)/delete/$',
view=TrashedDocumentDeleteView.as_view(), name='document_delete'
),
url(
regex=r'^multiple/delete/$',
view=TrashedDocumentDeleteView.as_view(),
name='document_multiple_delete'
),
]
urlpatterns = [
@@ -78,10 +116,6 @@ urlpatterns = [
view=RecentAddedDocumentListView.as_view(),
name='document_list_recent_added'
),
url(
regex=r'^list/deleted/$', view=DeletedDocumentListView.as_view(),
name='document_list_deleted'
),
url(
regex=r'^list/duplicated/$',
view=DuplicatedDocumentListView.as_view(),
@@ -100,23 +134,6 @@ urlpatterns = [
view=DocumentDuplicatesListView.as_view(),
name='document_duplicates_list'
),
url(
regex=r'^(?P<pk>\d+)/restore/$', view=DocumentRestoreView.as_view(),
name='document_restore'
),
url(
regex=r'^multiple/restore/$', view=DocumentRestoreManyView.as_view(),
name='document_multiple_restore'
),
url(
regex=r'^(?P<pk>\d+)/delete/$',
view=DeletedDocumentDeleteView.as_view(), name='document_delete'
),
url(
regex=r'^multiple/delete/$',
view=DeletedDocumentDeleteManyView.as_view(),
name='document_multiple_delete'
),
url(
regex=r'^(?P<pk>\d+)/type/$',
view=DocumentDocumentTypeEditView.as_view(),
@@ -126,14 +143,6 @@ urlpatterns = [
regex=r'^multiple/type/$', view=DocumentDocumentTypeEditView.as_view(),
name='document_multiple_document_type_edit'
),
url(
regex=r'^(?P<pk>\d+)/trash/$', view=DocumentTrashView.as_view(),
name='document_trash'
),
url(
regex=r'^multiple/trash/$', view=DocumentTrashManyView.as_view(),
name='document_multiple_trash'
),
url(
regex=r'^(?P<pk>\d+)/edit/$', view=DocumentEditView.as_view(),
name='document_edit'
@@ -218,11 +227,6 @@ urlpatterns = [
regex=r'^cache/clear/$', view=ClearImageCacheView.as_view(),
name='document_clear_image_cache'
),
url(
regex=r'^trash_can/empty/$', view=EmptyTrashCanView.as_view(),
name='trash_can_empty'
),
url(
regex=r'^page/(?P<pk>\d+)/$', view=DocumentPageView.as_view(),
name='document_page_view'
@@ -323,7 +327,7 @@ urlpatterns = [
),
]
urlpatterns.extend(urlpatterns_favorite_documents)
urlpatterns.extend(urlpatterns_trashed_documents)
api_urls = [
url(
@@ -384,7 +388,7 @@ api_urls = [
),
url(
regex=r'^trashed_documents/$',
view=APIDeletedDocumentListView.as_view(), name='trasheddocument-list'
view=APITrashedDocumentListView.as_view(), name='trasheddocument-list'
),
url(
regex=r'^trashed_documents/(?P<pk>[0-9]+)/$',

View File

@@ -38,8 +38,8 @@ logger = logging.getLogger(__name__)
class DocumentPageListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access(
permissions=permission_document_view, user=self.request.user,
obj=self.get_document()
obj=self.get_document(), permissions=(permission_document_view,),
user=self.request.user
)
return super(
@@ -66,8 +66,8 @@ class DocumentPageNavigationBase(RedirectView):
document_page = self.get_object()
AccessControlList.objects.check_access(
permissions=permission_document_view, user=request.user,
obj=document_page.document
obj=document_page.document,
permissions=(permission_document_view,), user=request.user
)
return super(DocumentPageNavigationBase, self).dispatch(
@@ -170,8 +170,8 @@ class DocumentPageView(SimpleView):
def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access(
permissions=permission_document_view, user=request.user,
obj=self.get_object().document
obj=self.get_object().document,
permissions=(permission_document_view,), user=request.user
)
return super(
@@ -214,11 +214,11 @@ class DocumentPageViewResetView(RedirectView):
class DocumentPageInteractiveTransformation(RedirectView):
def dispatch(self, request, *args, **kwargs):
object = self.get_object()
obj = self.get_object()
AccessControlList.objects.check_access(
permissions=permission_document_view, user=request.user,
obj=object
obj=obj, permissions=(permission_document_view,),
user=request.user
)
return super(DocumentPageInteractiveTransformation, self).dispatch(

View File

@@ -138,8 +138,9 @@ class DocumentTypeFilenameCreateView(SingleObjectCreateView):
def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access(
permissions=permission_document_type_edit, user=request.user,
obj=self.get_document_type()
obj=self.get_document_type(),
permissions=(permission_document_type_edit,),
user=request.user
)
return super(DocumentTypeFilenameCreateView, self).dispatch(

View File

@@ -32,8 +32,9 @@ logger = logging.getLogger(__name__)
class DocumentVersionListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access(
permissions=permission_document_version_view, user=request.user,
obj=self.get_document()
obj=self.get_document(),
permissions=(permission_document_version_view,),
user=request.user
)
self.get_document().add_as_recent_document_for_user(request.user)

View File

@@ -318,8 +318,8 @@ class DocumentDownloadView(SingleObjectDownloadView):
class DocumentDuplicatesListView(DocumentListView):
def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access(
permissions=permission_document_view, user=self.request.user,
obj=self.get_document()
obj=self.get_document(), permissions=(permission_document_view,),
user=self.request.user
)
return super(
@@ -582,8 +582,8 @@ class DocumentTransformationsCloneView(FormView):
instance = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_transformation_edit,
user=self.request.user, obj=instance
obj=instance, permissions=(permission_transformation_edit,),
user=self.request.user
)
instance.add_as_recent_document_for_user(self.request.user)
@@ -597,8 +597,8 @@ class DocumentPrint(FormView):
def dispatch(self, request, *args, **kwargs):
instance = self.get_object()
AccessControlList.objects.check_access(
permissions=permission_document_print, user=self.request.user,
obj=instance
obj=instance, permissions=(permission_document_print,),
user=self.request.user
)
instance.add_as_recent_document_for_user(self.request.user)

View File

@@ -3,13 +3,13 @@ from __future__ import absolute_import, unicode_literals
import logging
from django.contrib import messages
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _, ungettext
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import ConfirmView
from mayan.apps.common.mixins import MultipleInstanceActionMixin
from mayan.apps.common.generics import (
ConfirmView, MultipleObjectConfirmActionView
)
from ..icons import icon_document_list_deleted
from ..models import DeletedDocument, Document
@@ -23,64 +23,100 @@ from ..tasks import task_delete_document
from .document_views import DocumentListView
__all__ = (
'DeletedDocumentDeleteView', 'DeletedDocumentDeleteManyView',
'DeletedDocumentListView', 'DocumentRestoreView', 'DocumentRestoreManyView',
'DocumentTrashView', 'DocumentTrashManyView', 'EmptyTrashCanView'
'DocumentTrashView', 'EmptyTrashCanView', 'TrashedDocumentDeleteView',
'TrashedDocumentListView', 'TrashedDocumentRestoreView'
)
logger = logging.getLogger(__name__)
class DeletedDocumentDeleteView(ConfirmView):
class DocumentTrashView(MultipleObjectConfirmActionView):
model = Document
object_permission = permission_document_trash
pk_url_kwarg = 'pk'
success_message_singular = _(
'%(count)d document moved to the trash.'
)
success_message_plural = _(
'%(count)d documents moved to the trash.'
)
def get_extra_context(self):
queryset = self.get_object_list()
result = {
'title': ungettext(
single='Move the selected document to the trash?',
plural='Move the selected documents to the trash?',
number=queryset.count()
)
}
return result
def object_action(self, form, instance):
instance.delete()
class EmptyTrashCanView(ConfirmView):
extra_context = {
'title': _('Delete the selected document?')
'title': _('Empty trash?')
}
def object_action(self, instance):
source_document = get_object_or_404(
klass=Document.passthrough, pk=instance.pk
)
AccessControlList.objects.check_access(
permissions=permission_document_delete, user=self.request.user,
obj=source_document
)
task_delete_document.apply_async(
kwargs={'deleted_document_id': instance.pk}
)
view_permission = permission_empty_trash
action_cancel_redirect = post_action_redirect = reverse_lazy(
'documents:document_list_deleted'
)
def view_action(self):
instance = get_object_or_404(
klass=DeletedDocument, pk=self.kwargs['pk']
)
self.object_action(instance=instance)
messages.success(
self.request, _('Document: %(document)s deleted.') % {
'document': instance
}
)
for deleted_document in DeletedDocument.objects.all():
task_delete_document.apply_async(
kwargs={'trashed_document_id': deleted_document.pk}
)
messages.success(self.request, _('Trash emptied successfully'))
class DeletedDocumentDeleteManyView(MultipleInstanceActionMixin, DeletedDocumentDeleteView):
extra_context = {
'title': _('Delete the selected documents?')
}
class TrashedDocumentDeleteView(MultipleObjectConfirmActionView):
model = DeletedDocument
success_message = '%(count)d document deleted.'
success_message_plural = '%(count)d documents deleted.'
object_permission = permission_document_delete
pk_url_kwarg = 'pk'
success_message_singular = _(
'%(count)d trashed document deleted.'
)
success_message_plural = _(
'%(count)d trashed documents deleted.'
)
def get_extra_context(self):
queryset = self.get_object_list()
result = {
'title': ungettext(
single='Delete the selected trashed document?',
plural='Delete the selected trashed documents?',
number=queryset.count()
)
}
return result
def object_action(self, form, instance):
task_delete_document.apply_async(
kwargs={'trashed_document_id': instance.pk}
)
class DeletedDocumentListView(DocumentListView):
class TrashedDocumentListView(DocumentListView):
object_permission = None
def get_document_queryset(self):
return AccessControlList.objects.filter_by_access(
permission_document_view, self.request.user,
queryset=DeletedDocument.trash.all()
permission=permission_document_view,
queryset=DeletedDocument.trash.all(),
user=self.request.user
)
def get_extra_context(self):
context = super(DeletedDocumentListView, self).get_extra_context()
context = super(TrashedDocumentListView, self).get_extra_context()
context.update(
{
'hide_link': True,
@@ -99,103 +135,29 @@ class DeletedDocumentListView(DocumentListView):
return context
class DocumentRestoreView(ConfirmView):
extra_context = {
'title': _('Restore the selected document?')
}
def object_action(self, instance):
source_document = get_object_or_404(
klass=Document.passthrough, pk=instance.pk
)
AccessControlList.objects.check_access(
permissions=permission_document_restore, user=self.request.user,
obj=source_document
)
instance.restore()
def view_action(self):
instance = get_object_or_404(
klass=DeletedDocument, pk=self.kwargs['pk']
)
self.object_action(instance=instance)
messages.success(
self.request, _('Document: %(document)s restored.') % {
'document': instance
}
)
class DocumentRestoreManyView(MultipleInstanceActionMixin, DocumentRestoreView):
extra_context = {
'title': _('Restore the selected documents?')
}
class TrashedDocumentRestoreView(MultipleObjectConfirmActionView):
model = DeletedDocument
success_message = '%(count)d document restored.'
success_message_plural = '%(count)d documents restored.'
class DocumentTrashView(ConfirmView):
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _('Move "%s" to the trash?') % self.get_object()
}
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
def get_post_action_redirect(self):
return reverse('documents:document_list_recent_access')
def object_action(self, instance):
AccessControlList.objects.check_access(
permissions=permission_document_trash, user=self.request.user,
obj=instance
)
instance.delete()
def view_action(self):
instance = self.get_object()
self.object_action(instance=instance)
messages.success(
self.request, _('Document: %(document)s moved to trash successfully.') % {
'document': instance
}
)
class DocumentTrashManyView(MultipleInstanceActionMixin, DocumentTrashView):
model = Document
success_message = '%(count)d document moved to the trash.'
success_message_plural = '%(count)d documents moved to the trash.'
def get_extra_context(self):
return {
'title': _('Move the selected documents to the trash?')
}
class EmptyTrashCanView(ConfirmView):
extra_context = {
'title': _('Empty trash?')
}
view_permission = permission_empty_trash
action_cancel_redirect = post_action_redirect = reverse_lazy(
'documents:document_list_deleted'
object_permission = permission_document_restore
pk_url_kwarg = 'pk'
success_message_singular = _(
'%(count)d trashed document restored.'
)
success_message_plural = _(
'%(count)d trashed documents restored.'
)
def view_action(self):
for deleted_document in DeletedDocument.objects.all():
task_delete_document.apply_async(
kwargs={'deleted_document_id': deleted_document.pk}
)
def get_extra_context(self):
queryset = self.get_object_list()
messages.success(self.request, _('Trash emptied successfully'))
result = {
'title': ungettext(
single='Restore the selected trashed document?',
plural='Restore the selected trashed documents?',
number=queryset.count()
)
}
return result
def object_action(self, form, instance):
instance.restore()