diff options
author | Jenkins <jenkins@review.openstack.org> | 2012-08-12 10:47:12 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2012-08-12 10:47:12 +0000 |
commit | b0066309fd57c0d20f754d3ecea94b617235f40e (patch) | |
tree | 09c6c775ee6dc5410ed186cae3909e99a8e69d3a /horizon/dashboards/nova | |
parent | caf166eacba9bb4f47fc668a8ea74688cd4e4b5e (diff) | |
parent | 89d3d11cb1fc237b9366d630ab67de49b338e7a6 (diff) | |
download | tuskar-ui-b0066309fd57c0d20f754d3ecea94b617235f40e.tar.gz |
Merge "Adds ResourceBrowser and ResourceBrowserView class"
Diffstat (limited to 'horizon/dashboards/nova')
10 files changed, 193 insertions, 166 deletions
diff --git a/horizon/dashboards/nova/containers/browsers.py b/horizon/dashboards/nova/containers/browsers.py new file mode 100644 index 00000000..0b986a8c --- /dev/null +++ b/horizon/dashboards/nova/containers/browsers.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import browsers +from .tables import ContainersTable, ObjectsTable + + +LOG = logging.getLogger(__name__) + + +class ContainerBrowser(browsers.ResourceBrowser): + name = "swift" + verbose_name = _("Swift") + navigation_table_class = ContainersTable + content_table_class = ObjectsTable diff --git a/horizon/dashboards/nova/containers/forms.py b/horizon/dashboards/nova/containers/forms.py index 9be240b8..bde2f32c 100644 --- a/horizon/dashboards/nova/containers/forms.py +++ b/horizon/dashboards/nova/containers/forms.py @@ -114,7 +114,7 @@ class CopyObject(forms.SelfHandlingForm): self.fields['new_container_name'].choices = containers def handle(self, request, data): - object_index = "horizon:nova:containers:object_index" + object_index = "horizon:nova:containers:index" orig_container = data['orig_container_name'] orig_object = data['orig_object_name'] new_container = data['new_container_name'] diff --git a/horizon/dashboards/nova/containers/tables.py b/horizon/dashboards/nova/containers/tables.py index 72ae557f..65b2db35 100644 --- a/horizon/dashboards/nova/containers/tables.py +++ b/horizon/dashboards/nova/containers/tables.py @@ -25,14 +25,21 @@ from django.utils.translation import ugettext_lazy as _ from horizon import api from horizon import messages from horizon import tables +from horizon.api import FOLDER_DELIMITER +from horizon.tables import DataTable LOG = logging.getLogger(__name__) +def wrap_delimiter(name): + return name + FOLDER_DELIMITER + + class DeleteContainer(tables.DeleteAction): data_type_singular = _("Container") data_type_plural = _("Containers") + completion_url = "horizon:nova:containers:index" def delete(self, request, obj_id): try: @@ -42,6 +49,18 @@ class DeleteContainer(tables.DeleteAction): _('Containers must be empty before deletion.')) raise + def get_success_url(self, request=None): + """ + Returns the URL to redirect to after a successful action. + """ + current_container = self.table.kwargs.get("container_name", None) + + # If the current_container is deleted, then redirect to the default + # completion url + if current_container in self.success_ids: + return self.completion_url + return request.get_full_path() + class CreateContainer(tables.LinkAction): name = "create" @@ -53,9 +72,14 @@ class CreateContainer(tables.LinkAction): class ListObjects(tables.LinkAction): name = "list_objects" verbose_name = _("View Container") - url = "horizon:nova:containers:object_index" + url = "horizon:nova:containers:index" classes = ("btn-list",) + def get_link_url(self, datum=None): + container_name = http.urlquote(datum.name) + args = (wrap_delimiter(container_name),) + return reverse(self.url, args=args) + class UploadObject(tables.LinkAction): name = "upload" @@ -76,6 +100,11 @@ class UploadObject(tables.LinkAction): (container_name, subfolders) if bit) return reverse(self.url, args=args) + def allowed(self, request, datum=None): + if self.table.kwargs.get('container_name', None): + return True + return False + def update(self, request, obj): # This will only be called for the row, so we can remove the button # styles meant for the table action version. @@ -86,15 +115,14 @@ def get_size_used(container): return filesizeformat(container.size_used) +def get_container_link(container): + return reverse("horizon:nova:containers:index", + args=(http.urlquote(wrap_delimiter(container.name)),)) + + class ContainersTable(tables.DataTable): - name = tables.Column("name", link='horizon:nova:containers:object_index', + name = tables.Column("name", link=get_container_link, verbose_name=_("Container Name")) - objects = tables.Column("object_count", - verbose_name=_('Objects'), - empty_value="0") - size = tables.Column(get_size_used, - verbose_name=_('Size'), - attrs={'data-type': 'size'}) def get_object_id(self, container): return container.name @@ -102,8 +130,9 @@ class ContainersTable(tables.DataTable): class Meta: name = "containers" verbose_name = _("Containers") - table_actions = (CreateContainer, DeleteContainer) + table_actions = (CreateContainer,) row_actions = (ListObjects, UploadObject, DeleteContainer) + browser_table = "navigation" class DeleteObject(tables.DeleteAction): @@ -127,8 +156,8 @@ class DeleteSubfolder(DeleteObject): class DeleteMultipleObjects(DeleteObject): name = "delete_multiple_objects" - data_type_singular = _("Object/Folder") - data_type_plural = _("Objects/Folders") + data_type_singular = _("Object") + data_type_plural = _("Objects") allowed_data_types = ("subfolders", "objects",) @@ -161,7 +190,7 @@ class ObjectFilterAction(tables.FilterAction): request = table._meta.request container = self.table.kwargs['container_name'] subfolder = self.table.kwargs['subfolder_path'] - path = subfolder + '/' if subfolder else '' + path = subfolder + FOLDER_DELIMITER if subfolder else '' self.filtered_data = api.swift_filter_objects(request, filter_string, container, @@ -178,9 +207,14 @@ class ObjectFilterAction(tables.FilterAction): return [datum for datum in data if datum.content_type != "application/directory"] + def allowed(self, request, datum=None): + if self.table.kwargs.get('container_name', None): + return True + return False + def sanitize_name(name): - return name.split("/")[-1] + return name.split(FOLDER_DELIMITER)[-1] def get_size(obj): @@ -188,9 +222,10 @@ def get_size(obj): def get_link_subfolder(subfolder): - return reverse("horizon:nova:containers:object_index", - args=(http.urlquote(subfolder.container.name), - http.urlquote(subfolder.name + "/"))) + container_name = subfolder.container.name + return reverse("horizon:nova:containers:index", + args=(http.urlquote(wrap_delimiter(container_name)), + http.urlquote(wrap_delimiter(subfolder.name)))) class CreateSubfolder(CreateContainer): @@ -200,9 +235,15 @@ class CreateSubfolder(CreateContainer): def get_link_url(self): container = self.table.kwargs['container_name'] subfolders = self.table.kwargs['subfolder_path'] - parent = "/".join((bit for bit in [container, subfolders] if bit)) - parent = parent.rstrip("/") - return reverse(self.url, args=(http.urlquote(parent + "/"),)) + parent = FOLDER_DELIMITER.join((bit for bit in [container, + subfolders] if bit)) + parent = parent.rstrip(FOLDER_DELIMITER) + return reverse(self.url, args=[http.urlquote(wrap_delimiter(parent))]) + + def allowed(self, request, datum=None): + if self.table.kwargs.get('container_name', None): + return True + return False class ObjectsTable(tables.DataTable): @@ -211,6 +252,7 @@ class ObjectsTable(tables.DataTable): allowed_data_types=("subfolders",), verbose_name=_("Object Name"), filters=(sanitize_name,)) + size = tables.Column(get_size, verbose_name=_('Size')) def get_object_id(self, obj): @@ -218,9 +260,10 @@ class ObjectsTable(tables.DataTable): class Meta: name = "objects" - verbose_name = _("Subfolders and Objects") + verbose_name = _("Objects") table_actions = (ObjectFilterAction, CreateSubfolder, UploadObject, DeleteMultipleObjects) row_actions = (DownloadObject, CopyObject, DeleteObject, DeleteSubfolder) data_types = ("subfolders", "objects") + browser_table = "content" diff --git a/horizon/dashboards/nova/containers/templates/containers/_copy.html b/horizon/dashboards/nova/containers/templates/containers/_copy.html index 870c8689..aef4431d 100644 --- a/horizon/dashboards/nova/containers/templates/containers/_copy.html +++ b/horizon/dashboards/nova/containers/templates/containers/_copy.html @@ -20,5 +20,5 @@ {% block modal-footer %} <input class="btn btn-primary pull-right" type="submit" value="{% trans "Copy Object" %}" /> - <a href="{% url horizon:nova:containers:object_index container_name %}" class="btn secondary cancel close">{% trans "Cancel" %}</a> + <a href="{% url horizon:nova:containers:index container_name|add:'/' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a> {% endblock %} diff --git a/horizon/dashboards/nova/containers/templates/containers/_upload.html b/horizon/dashboards/nova/containers/templates/containers/_upload.html index 22d137ae..fd838571 100644 --- a/horizon/dashboards/nova/containers/templates/containers/_upload.html +++ b/horizon/dashboards/nova/containers/templates/containers/_upload.html @@ -21,5 +21,5 @@ {% block modal-footer %} <input class="btn btn-primary pull-right" type="submit" value="{% trans "Upload Object" %}" /> - <a href="{% url horizon:nova:containers:object_index container_name %}" class="btn secondary cancel close">{% trans "Cancel" %}</a> + <a href="{% url horizon:nova:containers:index container_name|add:'/' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a> {% endblock %} diff --git a/horizon/dashboards/nova/containers/templates/containers/detail.html b/horizon/dashboards/nova/containers/templates/containers/detail.html deleted file mode 100644 index a2e15f49..00000000 --- a/horizon/dashboards/nova/containers/templates/containers/detail.html +++ /dev/null @@ -1,32 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% block title %}{% trans "Objects" %}{% endblock %} - -{% block page_header %} - <div class='page-header'> - <h2>{% trans "Container" %}: - {% if subfolders %} - <a href="{% url horizon:nova:containers:object_index container_name=container_name %}">{{container_name}}</a> - <small>/</small> - {% else %} - {{container_name}} - {% endif %} - {% for subfolder, path in subfolders %} - <small> - {% if not forloop.last %} - <a href="{% url horizon:nova:containers:object_index container_name=container_name subfolder_path=path %}"> - {% endif %}{{ subfolder }}{% if not forloop.last %}</a> /{% endif %} - </small> - {% endfor %} - </h2> - </div> -{% endblock page_header %} - -{% block main %} - <div id="subfolders"> - {{ subfolders_table.render }} - </div> - <div id="objects"> - {{ objects_table.render }} - </div> -{% endblock %} diff --git a/horizon/dashboards/nova/containers/templates/containers/index.html b/horizon/dashboards/nova/containers/templates/containers/index.html index 2060972c..1dc45d98 100644 --- a/horizon/dashboards/nova/containers/templates/containers/index.html +++ b/horizon/dashboards/nova/containers/templates/containers/index.html @@ -3,9 +3,25 @@ {% block title %}Containers{% endblock %} {% block page_header %} - {% include "horizon/common/_page_header.html" with title=_("Containers") %} + <div class='page-header'> + <h2>{% trans "Container" %} + {% if subfolders %} + : <a href="{% url horizon:nova:containers:index container_name|add:'/' %}">{{container_name}}</a> + <small>/</small> + {% elif container_name %} + : {{container_name}} + {% endif %} + {% for subfolder, path in subfolders %} + <small> + {% if not forloop.last %} + <a href="{% url horizon:nova:containers:index container_name|add:'/' path %}"> + {% endif %}{{ subfolder }}{% if not forloop.last %}</a> /{% endif %} + </small> + {% endfor %} + </h2> + </div> {% endblock page_header %} {% block main %} - {{ table.render }} + {{ swift_browser.render }} {% endblock %} diff --git a/horizon/dashboards/nova/containers/tests.py b/horizon/dashboards/nova/containers/tests.py index 7e7bbe8d..a39ac220 100644 --- a/horizon/dashboards/nova/containers/tests.py +++ b/horizon/dashboards/nova/containers/tests.py @@ -28,7 +28,7 @@ from mox import IsA from horizon import api from horizon import test -from .tables import ContainersTable, ObjectsTable +from .tables import ContainersTable, ObjectsTable, wrap_delimiter from . import forms @@ -93,50 +93,30 @@ class ContainerViewTests(test.TestCase): 'method': forms.CreateContainer.__name__} res = self.client.post(reverse('horizon:nova:containers:create'), formData) - url = reverse('horizon:nova:containers:object_index', - args=[self.containers.first().name]) + url = reverse('horizon:nova:containers:index', + args=[wrap_delimiter(self.containers.first().name)]) self.assertRedirectsNoFollow(res, url) -class ObjectViewTests(test.TestCase): - @test.create_stubs({api: ('swift_get_objects',)}) +class IndexViewTests(test.TestCase): def test_index(self): + self.mox.StubOutWithMock(api, 'swift_get_containers') + self.mox.StubOutWithMock(api, 'swift_get_objects') + containers = (self.containers.list(), False) ret = (self.objects.list(), False) + api.swift_get_containers(IsA(http.HttpRequest), + marker=None).AndReturn(containers) api.swift_get_objects(IsA(http.HttpRequest), self.containers.first().name, marker=None, path=None).AndReturn(ret) self.mox.ReplayAll() - res = self.client.get(reverse('horizon:nova:containers:object_index', - args=[self.containers.first().name])) - self.assertEquals(res.context['container_name'], - self.containers.first().name) - self.assertTemplateUsed(res, 'nova/containers/detail.html') - # UTF8 encoding here to ensure there aren't problems with Nose output. - expected = [obj.name.encode('utf8') for obj in self.objects.list()] - self.assertQuerysetEqual(res.context['objects_table'].data, - expected, - lambda obj: obj.name.encode('utf8')) - - @test.create_stubs({api: ('swift_get_objects',)}) - def test_index_subfolders(self): - ret = (self.objects.list(), False) - api.swift_get_objects(IsA(http.HttpRequest), - self.containers.first().name, - marker=None, - path='sub1/sub2').AndReturn(ret) - self.mox.ReplayAll() - - res = self.client.get(reverse('horizon:nova:containers:object_index', - args=[self.containers.first().name, - u'sub1/sub2/'])) - self.assertEquals(res.context['container_name'], - self.containers.first().name) - self.assertListEqual(res.context['subfolders'], - [('sub1', 'sub1/'), - ('sub2', 'sub1/sub2/'), ]) - self.assertTemplateUsed(res, 'nova/containers/detail.html') + res = self.client.get(reverse('horizon:nova:containers:index', + args=[wrap_delimiter(self.containers + .first() + .name)])) + self.assertTemplateUsed(res, 'nova/containers/index.html') # UTF8 encoding here to ensure there aren't problems with Nose output. expected = [obj.name.encode('utf8') for obj in self.objects.list()] self.assertQuerysetEqual(res.context['objects_table'].data, @@ -177,8 +157,8 @@ class ObjectViewTests(test.TestCase): 'object_file': temp_file} res = self.client.post(upload_url, formData) - index_url = reverse('horizon:nova:containers:object_index', - args=[container.name]) + index_url = reverse('horizon:nova:containers:index', + args=[wrap_delimiter(container.name)]) self.assertRedirectsNoFollow(res, index_url) # Test invalid filename @@ -197,8 +177,8 @@ class ObjectViewTests(test.TestCase): def test_delete(self): container = self.containers.first() obj = self.objects.first() - index_url = reverse('horizon:nova:containers:object_index', - args=[container.name]) + index_url = reverse('horizon:nova:containers:index', + args=[wrap_delimiter(container.name)]) self.mox.StubOutWithMock(api, 'swift_delete_object') api.swift_delete_object(IsA(http.HttpRequest), container.name, @@ -269,6 +249,6 @@ class ObjectViewTests(test.TestCase): copy_url = reverse('horizon:nova:containers:object_copy', args=[container_1.name, obj.name]) res = self.client.post(copy_url, formData) - index_url = reverse('horizon:nova:containers:object_index', - args=[container_2.name]) + index_url = reverse('horizon:nova:containers:index', + args=[wrap_delimiter(container_2.name)]) self.assertRedirectsNoFollow(res, index_url) diff --git a/horizon/dashboards/nova/containers/urls.py b/horizon/dashboards/nova/containers/urls.py index c72f1fc1..363b19c7 100644 --- a/horizon/dashboards/nova/containers/urls.py +++ b/horizon/dashboards/nova/containers/urls.py @@ -20,22 +20,19 @@ from django.conf.urls.defaults import patterns, url -from .views import IndexView, CreateView, UploadView, ObjectIndexView, CopyView +from .views import CreateView, UploadView, CopyView, ContainerView # Swift containers and objects. urlpatterns = patterns('horizon.dashboards.nova.containers.views', - url(r'^$', IndexView.as_view(), name='index'), + url(r'^((?P<container_name>.+?)/)?(?P<subfolder_path>(.+/)+)?$', + ContainerView.as_view(), name='index'), url(r'^(?P<container_name>(.+/)+)?create$', CreateView.as_view(), name='create'), - url(r'^(?P<container_name>[^/]+)/(?P<subfolder_path>(.+/)+)?$', - ObjectIndexView.as_view(), - name='object_index'), - - url(r'^(?P<container_name>[^/]+)/(?P<subfolder_path>(.+/)+)?upload$', + url(r'^(?P<container_name>.+?)/(?P<subfolder_path>(.+/)+)?upload$', UploadView.as_view(), name='object_upload'), diff --git a/horizon/dashboards/nova/containers/views.py b/horizon/dashboards/nova/containers/views.py index 91b9aaca..e2d5d9a8 100644 --- a/horizon/dashboards/nova/containers/views.py +++ b/horizon/dashboards/nova/containers/views.py @@ -21,7 +21,6 @@ """ Views for managing Swift containers. """ -import logging import os from django import http @@ -29,24 +28,20 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from horizon import api +from horizon import browsers from horizon import exceptions from horizon import forms -from horizon import tables +from horizon.api import FOLDER_DELIMITER +from .browsers import ContainerBrowser from .forms import CreateContainer, UploadObject, CopyObject -from .tables import ContainersTable, ObjectsTable +from .tables import wrap_delimiter -LOG = logging.getLogger(__name__) +class ContainerView(browsers.ResourceBrowserView): + browser_class = ContainerBrowser + template_name = "nova/containers/index.html" - -class IndexView(tables.DataTableView): - table_class = ContainersTable - template_name = 'nova/containers/index.html' - - def has_more_data(self, table): - return self._more - - def get_data(self): + def get_containers_data(self): containers = [] self._more = None marker = self.request.GET.get('marker', None) @@ -58,35 +53,6 @@ class IndexView(tables.DataTableView): exceptions.handle(self.request, msg) return containers - -class CreateView(forms.ModalFormView): - form_class = CreateContainer - template_name = 'nova/containers/create.html' - success_url = "horizon:nova:containers:object_index" - - def get_success_url(self): - parent = self.request.POST.get('parent', None) - if parent: - container, slash, remainder = parent.partition("/") - if remainder and not remainder.endswith("/"): - remainder = "".join([remainder, "/"]) - return reverse(self.success_url, args=(container, remainder)) - else: - return reverse(self.success_url, args=[self.request.POST['name']]) - - def get_initial(self): - initial = super(CreateView, self).get_initial() - initial['parent'] = self.kwargs['container_name'] - return initial - - -class ObjectIndexView(tables.MixedDataTableView): - table_class = ObjectsTable - template_name = 'nova/containers/detail.html' - - def has_more_data(self, table): - return self._more - @property def objects(self): """ Returns a list of objects given the subfolder's path. @@ -99,20 +65,20 @@ class ObjectIndexView(tables.MixedDataTableView): marker = self.request.GET.get('marker', None) container_name = self.kwargs['container_name'] subfolders = self.kwargs['subfolder_path'] - if subfolders: - prefix = subfolders.rstrip("/") - else: - prefix = None - try: - objects, self._more = api.swift_get_objects(self.request, - container_name, - marker=marker, - path=prefix) - except: - self._more = None - objects = [] - msg = _('Unable to retrieve object list.') - exceptions.handle(self.request, msg) + prefix = None + if container_name: + if subfolders: + prefix = subfolders.rstrip(FOLDER_DELIMITER) + try: + objects, self._more = api.swift_get_objects(self.request, + container_name, + marker=marker, + path=prefix) + except: + self._more = None + objects = [] + msg = _('Unable to retrieve object list.') + exceptions.handle(self.request, msg) self._objects = objects return self._objects @@ -134,7 +100,7 @@ class ObjectIndexView(tables.MixedDataTableView): return filtered_objects def get_context_data(self, **kwargs): - context = super(ObjectIndexView, self).get_context_data(**kwargs) + context = super(ContainerView, self).get_context_data(**kwargs) context['container_name'] = self.kwargs["container_name"] context['subfolders'] = [] if self.kwargs["subfolder_path"]: @@ -147,14 +113,38 @@ class ObjectIndexView(tables.MixedDataTableView): return context +class CreateView(forms.ModalFormView): + form_class = CreateContainer + template_name = 'nova/containers/create.html' + success_url = "horizon:nova:containers:index" + + def get_success_url(self): + parent = self.request.POST.get('parent', None) + if parent: + container, slash, remainder = parent.partition(FOLDER_DELIMITER) + container += FOLDER_DELIMITER + if remainder and not remainder.endswith(FOLDER_DELIMITER): + remainder = "".join([remainder, FOLDER_DELIMITER]) + return reverse(self.success_url, args=(container, remainder)) + else: + return reverse(self.success_url, args=[self.request.POST['name'] + + FOLDER_DELIMITER]) + + def get_initial(self): + initial = super(CreateView, self).get_initial() + initial['parent'] = self.kwargs['container_name'] + return initial + + class UploadView(forms.ModalFormView): form_class = UploadObject template_name = 'nova/containers/upload.html' - success_url = "horizon:nova:containers:object_index" + success_url = "horizon:nova:containers:index" def get_success_url(self): + container_name = self.request.POST['container_name'] return reverse(self.success_url, - args=(self.request.POST['container_name'], + args=(wrap_delimiter(container_name), self.request.POST.get('path', ''))) def get_initial(self): @@ -171,7 +161,7 @@ def object_download(request, container_name, object_path): obj = api.swift.swift_get_object(request, container_name, object_path) # Add the original file extension back on if it wasn't preserved in the # name given to the object. - filename = object_path.rsplit("/")[-1] + filename = object_path.rsplit(FOLDER_DELIMITER)[-1] if not os.path.splitext(obj.name)[1]: name, ext = os.path.splitext(obj.metadata.get('orig-filename', '')) filename = "%s%s" % (filename, ext) @@ -196,11 +186,12 @@ def object_download(request, container_name, object_path): class CopyView(forms.ModalFormView): form_class = CopyObject template_name = 'nova/containers/copy.html' - success_url = "horizon:nova:containers:object_index" + success_url = "horizon:nova:containers:index" def get_success_url(self): + new_container_name = self.request.POST['new_container_name'] return reverse(self.success_url, - args=(self.request.POST['new_container_name'], + args=(wrap_delimiter(new_container_name), self.request.POST.get('path', ''))) def get_form_kwargs(self): |