diff --git a/apps/common/managers.py b/apps/common/managers.py new file mode 100644 index 0000000000..729d73c007 --- /dev/null +++ b/apps/common/managers.py @@ -0,0 +1,24 @@ +from django.db import models + + +class CustomizableQuerySetManager(models.Manager): + # FROM: https://gist.github.com/2587518 + # AUTHOR: https://gist.github.com/fission6 + # http://docs.djangoproject.com/en/dev/topics/db/managers/#using-managers-for-related-object-access + # Not working cause of: + # http://code.djangoproject.com/ticket/9643 + use_for_related_fields = True + + def __init__(self, qs_class=models.query.QuerySet): + self.queryset_class = qs_class + super(CustomizableQuerySetManager, self).__init__() + + def get_query_set(self): + return self.queryset_class(self.model) + + def __getattr__(self, attr, *args): + try: + return getattr(self.__class__, attr, *args) + except AttributeError: + return getattr(self.get_query_set(), attr, *args) + diff --git a/apps/common/querysets.py b/apps/common/querysets.py new file mode 100644 index 0000000000..6fe4a7be30 --- /dev/null +++ b/apps/common/querysets.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import + +from django.db.models.query import QuerySet + +from .managers import CustomizableQuerySetManager + + +class CustomizableQuerySet(QuerySet): + """Base QuerySet class for adding custom methods that are made + available on both the manager and subsequent cloned QuerySets""" + + @classmethod + def as_manager(cls, ManagerClass=CustomizableQuerySetManager): + return ManagerClass(cls) diff --git a/apps/trash/__init__.py b/apps/trash/__init__.py index 6493b140cc..9c08d0af97 100644 --- a/apps/trash/__init__.py +++ b/apps/trash/__init__.py @@ -1,7 +1,28 @@ from __future__ import absolute_import +from django.utils.translation import ugettext_lazy as _ +from django.db import transaction, DatabaseError + from documents.models import Document +from navigation.api import bind_links +from project_tools.api import register_tool from .api import make_trashable +from .links import trash_can_list, trash_can_items, trash_can_item_restore +from .models import TrashCan, TrashCanItem -make_trashable(Document) +register_tool(trash_can_list) +bind_links(['trash_can_list', TrashCan, TrashCanItem], trash_can_list, menu_name='secondary_menu') +bind_links([TrashCan], trash_can_items) +bind_links([TrashCanItem], trash_can_item_restore) + +@transaction.commit_on_success +def create_trash_cans(): + try: + documents_trash_can, created = TrashCan.objects.get_or_create(name='documents', defaults={'label': _(u'documents')}) + except DatabaseError: + transaction.rollback() + else: + make_trashable(Document, documents_trash_can) + +create_trash_cans() diff --git a/apps/trash/admin.py b/apps/trash/admin.py index 03f25b7440..afa501d4d4 100644 --- a/apps/trash/admin.py +++ b/apps/trash/admin.py @@ -2,12 +2,12 @@ from __future__ import absolute_import from django.contrib import admin -from .models import TrashedItem +from .models import TrashCanItem -class TrashedItemAdmin(admin.ModelAdmin): +class TrashCanItemAdmin(admin.ModelAdmin): list_display = ('content_type', 'object_id', 'content_object',) list_display_links = ('content_object',) -admin.site.register(TrashedItem, TrashedItemAdmin) +admin.site.register(TrashCanItem, TrashCanItemAdmin) diff --git a/apps/trash/api.py b/apps/trash/api.py index bbba8a8ae0..aae3e6044d 100644 --- a/apps/trash/api.py +++ b/apps/trash/api.py @@ -1,15 +1,19 @@ from __future__ import absolute_import -from .models import TrashableModelManager, new_delete_method +from common.querysets import CustomizableQuerySet +from .models import new_delete_method, TrashableQuerySetManager trashable_models = [] -def make_trashable(model): + + +def make_trashable(model, trash_can): trashable_models.append(model) - #model.__class__.objects = TrashableModelManager() - #model.__class__._default_manager = TrashableModelManager() - #model.objects = TrashableModelManager() - model.add_to_class('objects', TrashableModelManager()) + + old_manager = getattr(model, '_default_manager') + model.add_to_class('objects', CustomizableQuerySet.as_manager(TrashableQuerySetManager)) + model._default_manager = model.objects + model.add_to_class('trash_passthru', old_manager) + old_delete_method = model.delete - model.delete = new_delete_method(old_delete_method) - #model.add_to_class('is_in_trash', return True) + model.delete = new_delete_method(trash_can, old_delete_method) diff --git a/apps/trash/links.py b/apps/trash/links.py new file mode 100644 index 0000000000..2aeaeb7098 --- /dev/null +++ b/apps/trash/links.py @@ -0,0 +1,10 @@ +from __future__ import absolute_import + +from django.utils.translation import ugettext_lazy as _ + +from navigation.api import Link + + +trash_can_list = Link(text=_(u'trash cans'), view='trash_can_list', sprite='bin_closed', icon='bin_closed.png') +trash_can_items = Link(text=_(u'items'), view='trash_can_items', args='object.pk', sprite='bin') +trash_can_item_restore = Link(text=_(u'restore'), view='trash_can_item_restore', args='object.pk', sprite='bin_empty') diff --git a/apps/trash/migrations/0001_initial.py b/apps/trash/migrations/0001_initial.py index 683362218d..df5c2ef63e 100644 --- a/apps/trash/migrations/0001_initial.py +++ b/apps/trash/migrations/0001_initial.py @@ -8,18 +8,36 @@ from django.db import models class Migration(SchemaMigration): def forwards(self, orm): - # Adding model 'TrashedItem' - db.create_table('trash_trasheditem', ( + # Adding model 'TrashCan' + db.create_table('trash_trashcan', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=32)), + )) + db.send_create_signal('trash', ['TrashCan']) + + # Adding model 'TrashCanItem' + db.create_table('trash_trashcanitem', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('trash_can', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['trash.TrashCan'])), + ('trashed_at', self.gf('django.db.models.fields.DateTimeField')()), ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), )) - db.send_create_signal('trash', ['TrashedItem']) + db.send_create_signal('trash', ['TrashCanItem']) + + # Adding unique constraint on 'TrashCanItem', fields ['trash_can', 'content_type', 'object_id'] + db.create_unique('trash_trashcanitem', ['trash_can_id', 'content_type_id', 'object_id']) def backwards(self, orm): - # Deleting model 'TrashedItem' - db.delete_table('trash_trasheditem') + # Removing unique constraint on 'TrashCanItem', fields ['trash_can', 'content_type', 'object_id'] + db.delete_unique('trash_trashcanitem', ['trash_can_id', 'content_type_id', 'object_id']) + + # Deleting model 'TrashCan' + db.delete_table('trash_trashcan') + + # Deleting model 'TrashCanItem' + db.delete_table('trash_trashcanitem') models = { @@ -30,11 +48,18 @@ class Migration(SchemaMigration): 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, - 'trash.trasheditem': { - 'Meta': {'object_name': 'TrashedItem'}, + 'trash.trashcan': { + 'Meta': {'object_name': 'TrashCan'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) + }, + 'trash.trashcanitem': { + 'Meta': {'unique_together': "(('trash_can', 'content_type', 'object_id'),)", 'object_name': 'TrashCanItem'}, 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'trash_can': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['trash.TrashCan']"}), + 'trashed_at': ('django.db.models.fields.DateTimeField', [], {}) } } diff --git a/apps/trash/models.py b/apps/trash/models.py index 21581c896e..a76ac32d6a 100644 --- a/apps/trash/models.py +++ b/apps/trash/models.py @@ -1,10 +1,63 @@ +import datetime + from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic from django.db import models from django.utils.translation import ugettext_lazy as _ +from common.managers import CustomizableQuerySetManager -class TrashedItemManager(models.Manager): + +class TrashCanManager(models.Manager): + def get_or_create(self, *args, **kwargs): + #job_queue_labels[kwargs.get('name')] = kwargs.get('defaults', {}).get('label') + instance, created = super(TrashCanManager, self).get_or_create(*args, **kwargs) + instance.label = kwargs.get('defaults', {}).get('label') + instance.save() + return instance, created + + +class TrashCan(models.Model): + trash_can_labels = {} + + name = models.CharField(max_length=32, verbose_name=_(u'name'), unique=True) + + objects = TrashCanManager() + + def __unicode__(self): + return unicode(self.label) or self.names + + def _get_label(self): + return TrashCan.trash_can_labels.get(self.name) + + def _set_label(self, value): + TrashCan.trash_can_labels[self.name] = value + + label = property(_get_label, _set_label) + + def put(self, obj): + # TODO: check if obj is trashable model + obj.delete() + + @property + def items(self): + return self.trashcanitem_set + + def empty(self): + self.items.all().delete() + + def save(self, *args, **kwargs): + label = getattr(self, 'label', None) + if label: + TrashCan.trash_can_labels[self.name] = label + return super(TrashCan, self).save(*args, **kwargs) + + class Meta: + verbose_name = _(u'trash can') + verbose_name_plural = _(u'trash cans') + + +class TrashCanItemManager(models.Manager): def is_in_trash(self, obj): content_type = ContentType.objects.get_for_model(obj) try: @@ -18,13 +71,14 @@ class TrashedItemManager(models.Manager): return [trash_item.object_id for trash_item in self.model.objects.all()] -class TrashedItem(models.Model): - #trashed_at = models.DateTimeField(_('Trashed at'), editable=False, blank=True, null=True) +class TrashCanItem(models.Model): + trash_can = models.ForeignKey(TrashCan, verbose_name=_(u'trash can')) + trashed_at = models.DateTimeField(verbose_name=_(u'trashed at'), editable=False) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') - objects = TrashedItemManager() + objects = TrashCanItemManager() def __unicode__(self): return unicode(self.content_object) @@ -32,21 +86,31 @@ class TrashedItem(models.Model): def restore(self): self.delete() + def save(self, *args, **kwargs): + if not self.pk: + self.trashed_at=datetime.datetime.now() + return super(TrashCanItem, self).save(*args, **kwargs) + + class Meta: + verbose_name = _(u'trash can item') + verbose_name_plural = _(u'trash can items') + unique_together = ('trash_can', 'content_type', 'object_id') -def new_delete_method(old_delete_method): + +class TrashableQuerySetManager(CustomizableQuerySetManager): + def get_query_set(self): + return super(TrashableQuerySetManager, self).get_query_set().exclude(pk__in=TrashCanItem.objects.ids()) + + +def new_delete_method(trash_can, old_delete_method): def delete(self, *args, **kwargs): trash = kwargs.pop('trash', True) if trash==False: return old_delete_method(self, *args, **kwargs) else: - trashed_item = TrashedItem.objects.create(content_object=self)#, trashed_at=datetime.now()) + #trashed_item = TrashedItem.objects.create(trash_can=trash_can, content_object=self, trashed_at=datetime.datetime.now()) + trashed_item = trash_can.items.create(trash_can=trash_can, content_object=self) return delete - -class TrashableModelManager(models.Manager): - def get_query_set(self): - print 'excluded', TrashedItem.objects.items() - query_set = super(TrashableModelManager, self).get_query_set().exclude(pk__in=TrashedItem.objects.ids()) - return query_set diff --git a/apps/trash/static/images/icons/bin_closed.png b/apps/trash/static/images/icons/bin_closed.png new file mode 100755 index 0000000000..18b0d917b5 Binary files /dev/null and b/apps/trash/static/images/icons/bin_closed.png differ diff --git a/apps/trash/static/images/icons/bin_empty.png b/apps/trash/static/images/icons/bin_empty.png new file mode 100755 index 0000000000..9bb299d64f Binary files /dev/null and b/apps/trash/static/images/icons/bin_empty.png differ diff --git a/apps/trash/urls.py b/apps/trash/urls.py index daeab811ad..ba8deaf2b8 100644 --- a/apps/trash/urls.py +++ b/apps/trash/urls.py @@ -1,4 +1,7 @@ from django.conf.urls.defaults import patterns, url urlpatterns = patterns('trash.views', + url(r'^list/$', 'trash_can_list', (), 'trash_can_list'), + url(r'^trash_can/(?P\d+)/items/$', 'trash_can_items', (), 'trash_can_items'), + url(r'^trash_can/item/(?P\d+)/restore/$', 'trash_can_item_restore', (), 'trash_can_item_restore'), ) diff --git a/apps/trash/views.py b/apps/trash/views.py index 60f00ef0ef..4f7b63df75 100644 --- a/apps/trash/views.py +++ b/apps/trash/views.py @@ -1 +1,89 @@ -# Create your views here. +from __future__ import absolute_import + +from django.contrib import messages +from django.shortcuts import render_to_response, get_object_or_404 +from django.template import RequestContext +from django.utils.translation import ugettext_lazy as _ + +#from permissions.models import Permission +#from common.utils import encapsulate + +#from .permissions import PERMISSION_VIEW_SCHEDULER_LIST, PERMISSION_VIEW_JOB_LIST + +from .models import TrashCan, TrashCanItem + + +def trash_can_list(request): + #Permission.objects.check_permissions(request.user, [PERMISSION_VIEW_JOB_LIST]) + + context = { + 'object_list': TrashCan.objects.all(), + 'title': _(u'trash cans'), + 'extra_columns': [ + { + 'name': _(u'name'), + 'attribute': 'name' + }, + { + 'name': _(u'label'), + 'attribute': 'label' + }, + { + 'name': _(u'items'), + 'attribute': 'items.count' + }, + ], + 'hide_object': True, + } + + return render_to_response('generic_list.html', context, + context_instance=RequestContext(request)) + + +def trash_can_items(request, trash_can_pk): + #Permission.objects.check_permissions(request.user, [PERMISSION_VIEW_JOB_LIST]) + + trash_can = get_object_or_404(TrashCan, pk=trash_can_pk) + + context = { + 'object_list': trash_can.items.all(), + 'object': trash_can, + 'title': _(u'items in trash can: %s') % trash_can, + 'extra_columns': [ + { + 'name': _(u'date time'), + 'attribute': 'trashed_at' + }, + ], + 'hide_link': True, + } + + return render_to_response('generic_list.html', context, + context_instance=RequestContext(request)) + + +def trash_can_item_restore(request, trash_can_item_pk): + #Permission.objects.check_permissions(request.user, [PERMISSION_OCR_QUEUE_ENABLE_DISABLE]) + + next = request.POST.get('next', request.GET.get('next', request.META.get('HTTP_REFERER', None))) + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', None))) + + trash_can_item = get_object_or_404(TrashCanItem, pk=trash_can_item_pk) + + if request.method == 'POST': + try: + trash_can_item.restore() + except Exception, exc: + messages.warning(request, _(u'Error restoring item; %s') % exc) + return HttpResponseRedirect(previous) + else: + messages.success(request, _(u'Item restored successfully.')) + return HttpResponseRedirect(next) + + return render_to_response('generic_confirm.html', { + 'object': trash_can_item, + 'title': _(u'Are you sure you wish to restore trash can item: %s?') % trash_can_item, + 'next': next, + 'previous': previous, + 'form_icon': 'bin_empty.png', + }, context_instance=RequestContext(request)) diff --git a/docs/releases/0.13.rst b/docs/releases/0.13.rst index 1012b877b3..3a36a675b5 100644 --- a/docs/releases/0.13.rst +++ b/docs/releases/0.13.rst @@ -87,6 +87,7 @@ Afterwards migrate existing database schema with:: $ ./manage.py migrate lock_manager 0001 --fake $ ./manage.py migrate job_processor $ ./manage.py migrate clustering + $ ./manage.py migrate trash Issue the following command to index existing documents in the new full text search database:: diff --git a/settings.py b/settings.py index fdd5d2ea68..878f60cc28 100644 --- a/settings.py +++ b/settings.py @@ -160,6 +160,7 @@ INSTALLED_APPS = ( 'django_gpg', 'acls', 'converter', + 'trash', 'user_management', 'mimetype', 'clustering', @@ -190,7 +191,6 @@ INSTALLED_APPS = ( 'rest_api', 'bootstrap', 'statistics', - 'trash', # Has to be last so the other apps can register it's signals 'signaler',