From 7f7ba8924d2dbe6707a726193621399c95e64eb8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 24 Sep 2012 17:34:30 -0400 Subject: [PATCH] Add CRUD views to bootstrap app, add support for dumping models into bootstrap fixtures --- apps/bootstrap/__init__.py | 8 +- apps/bootstrap/classes.py | 26 ++++++ apps/bootstrap/forms.py | 28 ++++++ apps/bootstrap/icons.py | 11 ++- apps/bootstrap/links.py | 19 ++++- apps/bootstrap/literals.py | 3 + apps/bootstrap/managers.py | 35 ++++++++ apps/bootstrap/models.py | 30 ++++++- apps/bootstrap/permissions.py | 7 +- apps/bootstrap/registry.py | 12 +-- apps/bootstrap/urls.py | 9 +- apps/bootstrap/views.py | 155 ++++++++++++++++++++++++++++++++-- 12 files changed, 312 insertions(+), 31 deletions(-) create mode 100644 apps/bootstrap/forms.py create mode 100644 apps/bootstrap/managers.py diff --git a/apps/bootstrap/__init__.py b/apps/bootstrap/__init__.py index 58a9ee0976..fbb7591803 100644 --- a/apps/bootstrap/__init__.py +++ b/apps/bootstrap/__init__.py @@ -2,7 +2,11 @@ from __future__ import absolute_import from navigation.api import bind_links -from .links import bootstrap_execute +from .links import (link_bootstrap_setup_create, link_bootstrap_setup_execute, + link_bootstrap_setup_list, link_bootstrap_setup_edit, link_bootstrap_setup_delete, + link_bootstrap_setup_view, link_bootstrap_setup_dump) from .models import BootstrapSetup -bind_links([BootstrapSetup], [bootstrap_execute]) +bind_links([BootstrapSetup], [link_bootstrap_setup_view, link_bootstrap_setup_edit, link_bootstrap_setup_delete, link_bootstrap_setup_execute]) +bind_links([BootstrapSetup], [link_bootstrap_setup_list, link_bootstrap_setup_create, link_bootstrap_setup_dump], menu_name='secondary_menu') +bind_links(['bootstrap_setup_list', 'bootstrap_setup_create', 'bootstrap_setup_dump'], [link_bootstrap_setup_list, link_bootstrap_setup_create, link_bootstrap_setup_dump], menu_name='secondary_menu') diff --git a/apps/bootstrap/classes.py b/apps/bootstrap/classes.py index c56133bfa1..e7556ba55e 100644 --- a/apps/bootstrap/classes.py +++ b/apps/bootstrap/classes.py @@ -16,3 +16,29 @@ class Cleanup(object): self.name = name self.function = function self.__class__._registry[self.name] = self + + +class BootstrapModel(object): + """ + Class used to keep track of all the models to be dumped to create a + bootstrap setup from the current setup in use + """ + _registry = {} + + @classmethod + def get_all(cls): + return cls._registry.values() + + def get_fullname(self): + return '.'.join([self.app_name, self.model_name]) + + def __init__(self, model_name, app_name=None): + app_name_splitted = None + if '.' in model_name: + app_name_splitted, model_name = model_name.split('.') + + self.app_name = app_name_splitted or app_name + if not self.app_name: + raise Exception('Pass either a dotted app plus model name or a model name and a separate app name') + self.model_name = model_name + self.__class__._registry[self.get_fullname()] = self diff --git a/apps/bootstrap/forms.py b/apps/bootstrap/forms.py new file mode 100644 index 0000000000..10a31300e8 --- /dev/null +++ b/apps/bootstrap/forms.py @@ -0,0 +1,28 @@ +from __future__ import absolute_import + +import logging + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from common.forms import DetailForm + +from .models import BootstrapSetup + +logger = logging.getLogger(__name__) + + +class BootstrapSetupForm(forms.ModelForm): + class Meta: + model = BootstrapSetup + + +class BootstrapSetupForm_view(DetailForm): + class Meta: + model = BootstrapSetup + + +class BootstrapSetupForm_dump(forms.ModelForm): + class Meta: + model = BootstrapSetup + exclude = ['fixture'] diff --git a/apps/bootstrap/icons.py b/apps/bootstrap/icons.py index 93de7176ee..c5213a7472 100644 --- a/apps/bootstrap/icons.py +++ b/apps/bootstrap/icons.py @@ -1,8 +1,13 @@ from __future__ import absolute_import -from icons.literals import DATABASE_LIGHTNING, RADIOACTIVITY +from icons.literals import WIZARD, LIGHTNING, RADIOACTIVITY, MAGIC_WAND_2 from icons import Icon -icon_database_bootstrap = Icon(DATABASE_LIGHTNING) -icon_bootstrap_execute = Icon(DATABASE_LIGHTNING) +icon_bootstrap_setup = Icon(WIZARD) +icon_bootstrap_setup_view = Icon(WIZARD) +icon_bootstrap_setup_create = Icon(WIZARD) +icon_bootstrap_setup_edit = Icon(WIZARD) +icon_bootstrap_setup_delete = Icon(WIZARD) +icon_bootstrap_setup_execute = Icon(LIGHTNING) +icon_bootstrap_setup_dump = Icon(MAGIC_WAND_2) icon_nuke_database = Icon(RADIOACTIVITY) diff --git a/apps/bootstrap/links.py b/apps/bootstrap/links.py index 95f32d74e3..6a2cc842b6 100644 --- a/apps/bootstrap/links.py +++ b/apps/bootstrap/links.py @@ -4,9 +4,20 @@ from django.utils.translation import ugettext_lazy as _ from navigation import Link -from .permissions import PERMISSION_BOOTSTRAP_EXECUTE, PERMISSION_NUKE_DATABASE -from .icons import icon_database_bootstrap, icon_bootstrap_execute, icon_nuke_database +from .permissions import (PERMISSION_BOOTSTRAP_VIEW, PERMISSION_BOOTSTRAP_CREATE, + PERMISSION_BOOTSTRAP_EDIT, PERMISSION_BOOTSTRAP_DELETE, + PERMISSION_BOOTSTRAP_EXECUTE, PERMISSION_BOOTSTRAP_DUMP, + PERMISSION_NUKE_DATABASE) +from .icons import (icon_bootstrap_setup, icon_bootstrap_setup_execute, icon_bootstrap_setup_create, + icon_bootstrap_setup_edit, icon_bootstrap_setup_delete, icon_bootstrap_setup_view, + icon_bootstrap_setup_dump, icon_nuke_database) -database_bootstrap = Link(text=_(u'bootstrap database'), view='bootstrap_type_list', icon=icon_database_bootstrap, permissions=[PERMISSION_BOOTSTRAP_EXECUTE]) -bootstrap_execute = Link(text=_(u'execute'), view='bootstrap_execute', args='object.pk', icon=icon_bootstrap_execute, permissions=[PERMISSION_BOOTSTRAP_EXECUTE]) +link_bootstrap_setup_tool = Link(text=_(u'bootstrap'), view='bootstrap_setup_list', icon=icon_bootstrap_setup, permissions=[PERMISSION_BOOTSTRAP_VIEW]) +link_bootstrap_setup_list = Link(text=_(u'bootstrap setup list'), view='bootstrap_setup_list', icon=icon_bootstrap_setup, permissions=[PERMISSION_BOOTSTRAP_VIEW]) +link_bootstrap_setup_create = Link(text=_(u'create new bootstrap setup'), view='bootstrap_setup_create', icon=icon_bootstrap_setup_create, permissions=[PERMISSION_BOOTSTRAP_CREATE]) +link_bootstrap_setup_edit = Link(text=_(u'edit'), view='bootstrap_setup_edit', args='object.pk', icon=icon_bootstrap_setup_edit, permissions=[PERMISSION_BOOTSTRAP_EDIT]) +link_bootstrap_setup_delete = Link(text=_(u'delete'), view='bootstrap_setup_delete', args='object.pk', icon=icon_bootstrap_setup_delete, permissions=[PERMISSION_BOOTSTRAP_DELETE]) +link_bootstrap_setup_view = Link(text=_(u'details'), view='bootstrap_setup_view', args='object.pk', icon=icon_bootstrap_setup_view, permissions=[PERMISSION_BOOTSTRAP_VIEW]) +link_bootstrap_setup_execute = Link(text=_(u'execute'), view='bootstrap_setup_execute', args='object.pk', icon=icon_bootstrap_setup_execute, permissions=[PERMISSION_BOOTSTRAP_EXECUTE]) +link_bootstrap_setup_dump = Link(text=_(u'dump current setup'), view='bootstrap_setup_dump', icon=icon_bootstrap_setup_dump, permissions=[PERMISSION_BOOTSTRAP_DUMP]) link_erase_database = Link(text=_(u'erase database'), view='erase_database_view', icon=icon_nuke_database, permissions=[PERMISSION_NUKE_DATABASE]) diff --git a/apps/bootstrap/literals.py b/apps/bootstrap/literals.py index 8d2e97d54c..886f2e8817 100644 --- a/apps/bootstrap/literals.py +++ b/apps/bootstrap/literals.py @@ -26,3 +26,6 @@ FIXTURE_TYPE_PK_NULLIFIER = { FIXTURE_TYPE_YAML: lambda x: re.sub('pk: [0-9]{1,5}', 'pk: null', x), FIXTURE_TYPE_XML: lambda x: re.sub('pk="[0-9]{1,5}"', 'pk=null', x), } + +COMMAND_LOADDATA = 'loaddata' +COMMAND_DUMPDATA = 'dumpdata' diff --git a/apps/bootstrap/managers.py b/apps/bootstrap/managers.py new file mode 100644 index 0000000000..30c455ef98 --- /dev/null +++ b/apps/bootstrap/managers.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import + +import logging + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from django.db import models +from django.core import management + +from .classes import BootstrapModel +from .literals import COMMAND_DUMPDATA + +logger = logging.getLogger(__name__) + + +class BootstrapSetupManager(models.Manager): + def explode(self, data): + """ + Gets a compressed and compacted bootstrap setup and creates a new + database BootstrapSetup instance + """ + pass + + def dump(cls, format): + models = [instance.get_fullname() for instance in BootstrapModel.get_all()] + logger.debug('models: %s' % models) + result = StringIO() + options = dict(indent=4, format=format, use_natural_keys=True, interactive=False, verbosity=0, stdout=result) + management.call_command(COMMAND_DUMPDATA, *models, **options) + result.seek(0) + logger.debug('result: %s' % result) + return result.read() diff --git a/apps/bootstrap/models.py b/apps/bootstrap/models.py index 1d70b2d030..fa42d776ee 100644 --- a/apps/bootstrap/models.py +++ b/apps/bootstrap/models.py @@ -7,18 +7,22 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core import management -from .literals import FIXTURE_TYPES_CHOICES, FIXTURE_FILE_TYPE +from .literals import (FIXTURE_TYPES_CHOICES, FIXTURE_FILE_TYPE, + FIXTURE_TYPE_PK_NULLIFIER, COMMAND_LOADDATA) +from .managers import BootstrapSetupManager class BootstrapSetup(models.Model): """ - Model to store the fixture for a pre configured setup + Model to store the fixture for a pre configured setup. """ name = models.CharField(max_length=128, verbose_name=_(u'name'), unique=True) description = models.TextField(verbose_name=_(u'description'), blank=True) - fixture = models.TextField(verbose_name=_(u'fixture')) + fixture = models.TextField(verbose_name=_(u'fixture'), help_text=_(u'These are the actual database structure creation instructions.')) type = models.CharField(max_length=16, verbose_name=_(u'type'), choices=FIXTURE_TYPES_CHOICES) + objects = BootstrapSetupManager() + def __unicode__(self): return self.name @@ -35,9 +39,27 @@ class BootstrapSetup(models.Model): with open(filepath, 'w') as file_handle: file_handle.write(self.fixture) - management.call_command('loaddata', filepath, verbosity=0) + management.call_command(COMMAND_LOADDATA, filepath, verbosity=0) os.unlink(filepath) + def compress(self): + """ + Return a compacted and compressed version of the BootstrapSetup + instance, meant for download. + """ + return '' + + def sanitize(self): + """ + Remove pk values + """ + self.fixture = FIXTURE_TYPE_PK_NULLIFIER[self.type](self.fixture) + + def save(self, *args, **kwargs): + self.sanitize() + return super(BootstrapSetup, self).save(*args, **kwargs) + class Meta: verbose_name = _(u'bootstrap setup') verbose_name_plural = _(u'bootstrap setups') + ordering = ['name'] diff --git a/apps/bootstrap/permissions.py b/apps/bootstrap/permissions.py index 46955433c6..f954e31c67 100644 --- a/apps/bootstrap/permissions.py +++ b/apps/bootstrap/permissions.py @@ -6,5 +6,10 @@ from permissions.models import PermissionNamespace, Permission namespace = PermissionNamespace('bootstrap', _(u'Database bootstrap')) -PERMISSION_BOOTSTRAP_EXECUTE = Permission.objects.register(namespace, 'bootstrap_execute', _(u'Execute document bootstraps')) +PERMISSION_BOOTSTRAP_VIEW = Permission.objects.register(namespace, 'bootstrap_view', _(u'View bootstrap setups')) +PERMISSION_BOOTSTRAP_CREATE = Permission.objects.register(namespace, 'bootstrap_create', _(u'Create bootstrap setups')) +PERMISSION_BOOTSTRAP_EDIT = Permission.objects.register(namespace, 'bootstrap_edit', _(u'Edit bootstrap setups')) +PERMISSION_BOOTSTRAP_DELETE = Permission.objects.register(namespace, 'bootstrap_delete', _(u'Delete bootstrap setups')) +PERMISSION_BOOTSTRAP_EXECUTE = Permission.objects.register(namespace, 'bootstrap_execute', _(u'Execute bootstrap setups')) +PERMISSION_BOOTSTRAP_DUMP = Permission.objects.register(namespace, 'bootstrap_dump', _(u'Dump the current project\s setup into a bootstrap setup')) PERMISSION_NUKE_DATABASE = Permission.objects.register(namespace, 'nuke_database', _(u'Erase the entire database and document storage')) diff --git a/apps/bootstrap/registry.py b/apps/bootstrap/registry.py index 4825519940..6af9f4a7f5 100644 --- a/apps/bootstrap/registry.py +++ b/apps/bootstrap/registry.py @@ -2,11 +2,11 @@ from __future__ import absolute_import from django.utils.translation import ugettext_lazy as _ -from .icons import icon_database_bootstrap -from .links import database_bootstrap, link_erase_database +from .icons import icon_bootstrap_setup +from .links import link_bootstrap_setup_tool, link_erase_database -label = _(u'Database bootstrap') -description = _(u'Provides pre configured setups for indexes, document types, tags.') +label = _(u'Bootstrap') +description = _(u'Provides pre configured setups for indexes, document types, tags, etc.') dependencies = ['app_registry', 'icons', 'navigation', 'documents', 'indexing', 'metadata', 'tags'] -icon = icon_database_bootstrap -setup_links = [database_bootstrap, link_erase_database] +icon = icon_bootstrap_setup +setup_links = [link_bootstrap_setup_tool, link_erase_database] diff --git a/apps/bootstrap/urls.py b/apps/bootstrap/urls.py index 7e58ef303f..8a91228f4f 100644 --- a/apps/bootstrap/urls.py +++ b/apps/bootstrap/urls.py @@ -1,7 +1,12 @@ from django.conf.urls.defaults import patterns, url urlpatterns = patterns('bootstrap.views', - url(r'^type/list/$', 'bootstrap_type_list', (), 'bootstrap_type_list'), - url(r'^(?P\d+)/execute/$', 'bootstrap_execute', (), 'bootstrap_execute'), + url(r'^setup/list/$', 'bootstrap_setup_list', (), 'bootstrap_setup_list'), + url(r'^setup/create/$', 'bootstrap_setup_create', (), 'bootstrap_setup_create'), + url(r'^setup/(?P\d+)/edit/$', 'bootstrap_setup_edit', (), 'bootstrap_setup_edit'), + url(r'^setup/(?P\d+)/delete/$', 'bootstrap_setup_delete', (), 'bootstrap_setup_delete'), + url(r'^setup/(?P\d+)/$', 'bootstrap_setup_view', (), 'bootstrap_setup_view'), + url(r'^setup/(?P\d+)/execute/$', 'bootstrap_setup_execute', (), 'bootstrap_setup_execute'), + url(r'^setup/dump/$', 'bootstrap_setup_dump', (), 'bootstrap_setup_dump'), url(r'^nuke/$', 'erase_database_view', (), 'erase_database_view'), ) diff --git a/apps/bootstrap/views.py b/apps/bootstrap/views.py index 6dfbf834a5..0134667f0b 100644 --- a/apps/bootstrap/views.py +++ b/apps/bootstrap/views.py @@ -10,20 +10,24 @@ from django.core.urlresolvers import reverse from permissions.models import Permission from .models import BootstrapSetup -from .classes import Cleanup -from .permissions import PERMISSION_BOOTSTRAP_EXECUTE, PERMISSION_NUKE_DATABASE -from .icons import icon_bootstrap_execute, icon_nuke_database +from .classes import Cleanup, BootstrapModel +from .permissions import (PERMISSION_BOOTSTRAP_VIEW, PERMISSION_BOOTSTRAP_CREATE, + PERMISSION_BOOTSTRAP_EDIT, PERMISSION_BOOTSTRAP_DELETE, + PERMISSION_BOOTSTRAP_EXECUTE, PERMISSION_NUKE_DATABASE, PERMISSION_BOOTSTRAP_DUMP) +from .icons import icon_bootstrap_setup_execute, icon_nuke_database, icon_bootstrap_setup_delete +from .forms import BootstrapSetupForm, BootstrapSetupForm_view, BootstrapSetupForm_dump -def bootstrap_type_list(request): - Permission.objects.check_permissions(request.user, [PERMISSION_BOOTSTRAP_EXECUTE]) +def bootstrap_setup_list(request): + Permission.objects.check_permissions(request.user, [PERMISSION_BOOTSTRAP_VIEW]) context = { 'object_list': BootstrapSetup.objects.all(), - 'title': _(u'database bootstrap setups'), + 'title': _(u'bootstrap setups'), 'hide_link': True, 'extra_columns': [ {'name': _(u'description'), 'attribute': 'description'}, + {'name': _(u'type'), 'attribute': 'get_type_display'}, ], } @@ -31,11 +35,119 @@ def bootstrap_type_list(request): context_instance=RequestContext(request)) -def bootstrap_execute(request, bootstrap_setup_pk): +def bootstrap_setup_create(request): + Permission.objects.check_permissions(request.user, [PERMISSION_BOOTSTRAP_CREATE]) + + if request.method == 'POST': + form = BootstrapSetupForm(request.POST) + if form.is_valid(): + bootstrap = form.save() + messages.success(request, _(u'Bootstrap created successfully')) + return HttpResponseRedirect(reverse('bootstrap_setup_list')) + else: + messages.error(request, _(u'Error creating bootstrap setup.')) + else: + form = BootstrapSetupForm() + + return render_to_response('generic_form.html', { + 'title': _(u'create bootstrap'), + 'form': form, + }, + context_instance=RequestContext(request)) + + +def bootstrap_setup_edit(request, bootstrap_setup_pk): + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + + bootstrap = get_object_or_404(BootstrapSetup, pk=bootstrap_setup_pk) + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_BOOTSTRAP_EDIT]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_BOOTSTRAP_EDIT, request.user, bootstrap) + + if request.method == 'POST': + form = BootstrapSetupForm(instance=bootstrap, data=request.POST) + if form.is_valid(): + form.save() + messages.success(request, _(u'Bootstrap setup edited successfully')) + return HttpResponseRedirect(previous) + else: + messages.error(request, _(u'Error editing bootstrap setup.')) + else: + form = BootstrapSetupForm(instance=bootstrap) + + return render_to_response('generic_form.html', { + 'title': _(u'edit bootstrap setup: %s') % bootstrap, + 'form': form, + 'object': bootstrap, + 'previous': previous, + 'object_name': _(u'bootstrap setup'), + }, + context_instance=RequestContext(request)) + + +def bootstrap_setup_delete(request, bootstrap_setup_pk): + bootstrap = get_object_or_404(BootstrapSetup, pk=bootstrap_setup_pk) + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_BOOTSTRAP_DELETE]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_BOOTSTRAP_DELETE, request.user, bootstrap) + + post_action_redirect = reverse('bootstrap_setup_list') + + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', '/'))) + + if request.method == 'POST': + try: + bootstrap.delete() + messages.success(request, _(u'Bootstrap setup: %s deleted successfully.') % bootstrap) + except Exception, e: + messages.error(request, _(u'Bootstrap setup: %(bootstrap)s delete error: %(error)s') % { + 'bootstrap': bootstrap, 'error': e}) + + return HttpResponseRedirect(reverse('bootstrap_setup_list')) + + context = { + 'object_name': _(u'bootstrap setup'), + 'delete_view': True, + 'previous': previous, + 'next': next, + 'object': bootstrap, + 'title': _(u'Are you sure you with to delete the bootstrap setup: %s?') % bootstrap, + 'form_icon': icon_bootstrap_setup_delete, + } + + return render_to_response('generic_confirm.html', context, + context_instance=RequestContext(request)) + + +def bootstrap_setup_view(request, bootstrap_setup_pk): + bootstrap = get_object_or_404(BootstrapSetup, pk=bootstrap_setup_pk) + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_BOOTSTRAP_VIEW]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_BOOTSTRAP_VIEW, request.user, bootstrap) + + form = BootstrapSetupForm_view(instance=bootstrap) + context = { + 'form': form, + 'object': bootstrap, + 'object_name': _(u'bootstrap setup'), + } + + return render_to_response('generic_detail.html', context, + context_instance=RequestContext(request)) + + +def bootstrap_setup_execute(request, bootstrap_setup_pk): Permission.objects.check_permissions(request.user, [PERMISSION_BOOTSTRAP_EXECUTE]) bootstrap_setup = get_object_or_404(BootstrapSetup, pk=bootstrap_setup_pk) - post_action_redirect = reverse('bootstrap_type_list') + post_action_redirect = reverse('bootstrap_setup_list') previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', '/'))) @@ -54,7 +166,7 @@ def bootstrap_execute(request, bootstrap_setup_pk): 'delete_view': False, 'previous': previous, 'next': next, - 'form_icon': icon_bootstrap_execute, + 'form_icon': icon_bootstrap_setup_execute, 'object': bootstrap_setup, } @@ -64,6 +176,31 @@ def bootstrap_execute(request, bootstrap_setup_pk): context_instance=RequestContext(request)) +def bootstrap_setup_dump(request): + Permission.objects.check_permissions(request.user, [PERMISSION_BOOTSTRAP_DUMP]) + + if request.method == 'POST': + form = BootstrapSetupForm_dump(request.POST) + if form.is_valid(): + bootstrap = form.save(commit=False) + try: + bootstrap.fixture = BootstrapSetup.objects.dump(format=bootstrap.type) + except Exception as exception: + messages.error(request, _(u'Error dumping bootstrap setup; %s') % exception) + else: + bootstrap.save() + messages.success(request, _(u'Bootstrap created successfully.')) + return HttpResponseRedirect(reverse('bootstrap_setup_list')) + else: + form = BootstrapSetupForm_dump() + + return render_to_response('generic_form.html', { + 'title': _(u'dump current setup into a bootstrap setup'), + 'form': form, + }, + context_instance=RequestContext(request)) + + def erase_database_view(request): Permission.objects.check_permissions(request.user, [PERMISSION_NUKE_DATABASE])