diff --git a/horizon/api/swift.py b/horizon/api/swift.py index 381851620..8d57e7fb1 100644 --- a/horizon/api/swift.py +++ b/horizon/api/swift.py @@ -20,109 +20,171 @@ import logging -import cloudfiles +import swiftclient + from django.conf import settings from django.utils.translation import ugettext as _ from horizon import exceptions -from horizon.api.base import url_for +from horizon.api.base import url_for, APIDictWrapper LOG = logging.getLogger(__name__) FOLDER_DELIMITER = "/" -class SwiftAuthentication(object): - """ Auth container in the format CloudFiles expects. """ - def __init__(self, storage_url, auth_token): - self.storage_url = storage_url - self.auth_token = auth_token +class Container(APIDictWrapper): + pass - def authenticate(self): - return (self.storage_url, '', self.auth_token) + +class StorageObject(APIDictWrapper): + def __init__(self, apidict, container_name, orig_name=None, data=None): + super(StorageObject, self).__init__(apidict) + self.container_name = container_name + self.orig_name = orig_name + self.data = data + + +class PseudoFolder(APIDictWrapper): + """ + Wrapper to smooth out discrepencies between swift "subdir" items + and swift pseudo-folder objects. + """ + + def __init__(self, apidict, container_name): + super(PseudoFolder, self).__init__(apidict) + self.container_name = container_name + + def _has_content_type(self): + content_type = self._apidict.get("content_type", None) + return content_type == "application/directory" + + @property + def name(self): + if self._has_content_type(): + return self._apidict['name'] + return self.subdir.rstrip(FOLDER_DELIMITER) + + @property + def bytes(self): + if self._has_content_type(): + return self._apidict['bytes'] + return None + + @property + def content_type(self): + return "application/directory" + + +def _objectify(items, container_name): + """ Splits a listing of objects into their appropriate wrapper classes. """ + objects = {} + subdir_markers = [] + + # Deal with objects and object pseudo-folders first, save subdirs for later + for item in items: + if item.get("content_type", None) == "application/directory": + objects[item['name']] = PseudoFolder(item, container_name) + elif item.get("subdir", None) is not None: + subdir_markers.append(PseudoFolder(item, container_name)) + else: + objects[item['name']] = StorageObject(item, container_name) + # Revisit subdirs to see if we have any non-duplicates + for item in subdir_markers: + if item.name not in objects.keys(): + objects[item.name] = item + return objects.values() def swift_api(request): endpoint = url_for(request, 'object-store') LOG.debug('Swift connection created using token "%s" and url "%s"' % (request.user.token.id, endpoint)) - auth = SwiftAuthentication(endpoint, request.user.token.id) - return cloudfiles.get_connection(auth=auth) + return swiftclient.client.Connection(None, + request.user.username, + None, + preauthtoken=request.user.token.id, + preauthurl=endpoint, + auth_version="2.0") def swift_container_exists(request, container_name): try: - swift_api(request).get_container(container_name) + swift_api(request).head_container(container_name) return True - except cloudfiles.errors.NoSuchContainer: + except swiftclient.client.ClientException: return False def swift_object_exists(request, container_name, object_name): - container = swift_api(request).get_container(container_name) - try: - container.get_object(object_name) + swift_api(request).head_object(container_name, object_name) return True - except cloudfiles.errors.NoSuchObject: + except swiftclient.client.ClientException: return False def swift_get_containers(request, marker=None): limit = getattr(settings, 'API_RESULT_LIMIT', 1000) - containers = swift_api(request).get_all_containers(limit=limit + 1, - marker=marker) - if(len(containers) > limit): - return (containers[0:-1], True) + headers, containers = swift_api(request).get_account(limit=limit + 1, + marker=marker, + full_listing=True) + container_objs = [Container(c) for c in containers] + if(len(container_objs) > limit): + return (container_objs[0:-1], True) else: - return (containers, False) + return (container_objs, False) def swift_create_container(request, name): if swift_container_exists(request, name): raise exceptions.AlreadyExists(name, 'container') - return swift_api(request).create_container(name) + swift_api(request).put_container(name) + return Container({'name': name}) def swift_delete_container(request, name): swift_api(request).delete_container(name) + return True -def swift_get_objects(request, container_name, prefix=None, path=None, - marker=None): - limit = getattr(settings, 'API_RESULT_LIMIT', 1000) - container = swift_api(request).get_container(container_name) - objects = container.get_objects(prefix=prefix, - marker=marker, - limit=limit + 1, - delimiter=FOLDER_DELIMITER, - path=path) - if(len(objects) > limit): - return (objects[0:-1], True) +def swift_get_objects(request, container_name, prefix=None, marker=None, + limit=None): + limit = limit or getattr(settings, 'API_RESULT_LIMIT', 1000) + kwargs = dict(prefix=prefix, + marker=marker, + limit=limit + 1, + delimiter=FOLDER_DELIMITER, + full_listing=True) + headers, objects = swift_api(request).get_container(container_name, + **kwargs) + object_objs = _objectify(objects, container_name) + + if(len(object_objs) > limit): + return (object_objs[0:-1], True) else: - return (objects, False) + return (object_objs, False) def swift_filter_objects(request, filter_string, container_name, prefix=None, - path=None, marker=None): - #FIXME(kewu): Cloudfiles currently has no filtering API, thus the marker - #parameter here won't actually help the pagination. For now I am just - #getting the largest number of objects from a container and filtering based - #on those objects. - limit = 10000 - container = swift_api(request).get_container(container_name) - objects = container.get_objects(prefix=prefix, - marker=marker, - limit=limit, - delimiter=FOLDER_DELIMITER, - path=path) + marker=None): + # FIXME(kewu): Swift currently has no real filtering API, thus the marker + # parameter here won't actually help the pagination. For now I am just + # getting the largest number of objects from a container and filtering + # based on those objects. + limit = 9999 + objects = swift_get_objects(request, + container_name, + prefix=prefix, + marker=marker, + limit=limit) filter_string_list = filter_string.lower().strip().split(' ') def matches_filter(obj): for q in filter_string_list: return wildcard_search(obj.name.lower(), q) - return filter(matches_filter, objects) + return filter(matches_filter, objects[0]) def wildcard_search(string, q): @@ -142,7 +204,7 @@ def wildcard_search(string, q): def swift_copy_object(request, orig_container_name, orig_object_name, new_container_name, new_object_name): try: - # FIXME(gabriel): Cloudfiles currently fails at unicode in the + # FIXME(gabriel): The swift currently fails at unicode in the # copy_to method, so to provide a better experience we check for # unicode here and pre-empt with an error message rather than # letting the call fail. @@ -153,42 +215,50 @@ def swift_copy_object(request, orig_container_name, orig_object_name, except UnicodeEncodeError: raise exceptions.HorizonException(_("Unicode is not currently " "supported for object copy.")) - container = swift_api(request).get_container(orig_container_name) if swift_object_exists(request, new_container_name, new_object_name): raise exceptions.AlreadyExists(new_object_name, 'object') - orig_obj = container.get_object(orig_object_name) - return orig_obj.copy_to(new_container_name, new_object_name) + headers = {"X-Copy-From": FOLDER_DELIMITER.join([orig_container_name, + orig_object_name])} + return swift_api(request).put_object(new_container_name, + new_object_name, + None, + headers=headers) def swift_create_subfolder(request, container_name, folder_name): - container = swift_api(request).get_container(container_name) - obj = container.create_object(folder_name) - obj.headers = {'content-type': 'application/directory', - 'content-length': 0} - obj.send('') - obj.sync_metadata() - return obj + headers = {'content-type': 'application/directory', + 'content-length': 0} + etag = swift_api(request).put_object(container_name, + folder_name, + None, + headers=headers) + obj_info = {'subdir': folder_name, 'etag': etag} + return PseudoFolder(obj_info, container_name) def swift_upload_object(request, container_name, object_name, object_file): - container = swift_api(request).get_container(container_name) - obj = container.create_object(object_name) - obj.send(object_file) - return obj + headers = {} + headers['X-Object-Meta-Orig-Filename'] = object_file.name + etag = swift_api(request).put_object(container_name, + object_name, + object_file, + headers=headers) + obj_info = {'name': object_name, 'bytes': object_file.size, 'etag': etag} + return StorageObject(obj_info, container_name) def swift_delete_object(request, container_name, object_name): - container = swift_api(request).get_container(container_name) - container.delete_object(object_name) + swift_api(request).delete_object(container_name, object_name) + return True def swift_get_object(request, container_name, object_name): - container = swift_api(request).get_container(container_name) - return container.get_object(object_name) - - -def swift_get_object_data(request, container_name, object_name): - container = swift_api(request).get_container(container_name) - return container.get_object(object_name).stream() + headers, data = swift_api(request).get_object(container_name, object_name) + orig_name = headers.get("x-object-meta-orig-filename") + obj_info = {'name': object_name, 'bytes': len(data)} + return StorageObject(obj_info, + container_name, + orig_name=orig_name, + data=data) diff --git a/horizon/browsers/base.py b/horizon/browsers/base.py index 635cdc3c4..e4a897920 100644 --- a/horizon/browsers/base.py +++ b/horizon/browsers/base.py @@ -15,6 +15,7 @@ # under the License. from django import template +from django.utils.translation import ugettext_lazy as _ from horizon.tables import DataTable from horizon.utils import html @@ -32,6 +33,7 @@ class ResourceBrowser(html.HTMLElement): A more verbose name for the browser meant for display purposes. .. attribute:: navigation_table_class + This table displays data on the left side of the browser. Set the ``navigation_table_class`` attribute with the desired :class:`~horizon.tables.DataTable` class. @@ -39,6 +41,7 @@ class ResourceBrowser(html.HTMLElement): ``"navigation"``. .. attribute:: content_table_class + This table displays data on the right side of the browser. Set the ``content_table_class`` attribute with the desired :class:`~horizon.tables.DataTable` class. @@ -59,44 +62,35 @@ class ResourceBrowser(html.HTMLElement): verbose_name = None navigation_table_class = None content_table_class = None + navigable_item_name = _("Navigation Item") template = "horizon/common/_resource_browser.html" context_var_name = "browser" - def __init__(self, request, tables=None, attrs=None, - **kwargs): + def __init__(self, request, tables_dict=None, attrs=None, **kwargs): super(ResourceBrowser, self).__init__() - self.name = getattr(self, "name", self.__class__.__name__) - self.verbose_name = getattr(self, "verbose_name", self.name.title()) + self.name = self.name or self.__class__.__name__ + self.verbose_name = self.verbose_name or self.name.title() self.request = request self.attrs.update(attrs or {}) - - self.navigation_table_class = getattr(self, "navigation_table_class", - None) + self.check_table_class(self.content_table_class, "content_table_class") self.check_table_class(self.navigation_table_class, "navigation_table_class") - - self.content_table_class = getattr(self, "content_table_class", - None) - self.check_table_class(self.content_table_class, - "content_table_class") - - self.set_tables(tables) + if tables_dict: + self.set_tables(tables_dict) def check_table_class(self, cls, attr_name): - if not cls or not issubclass(cls, (DataTable, )): - raise ValueError("You must specify a DataTable class for " - "the %s attribute on %s " + if not cls or not issubclass(cls, DataTable): + raise ValueError("You must specify a DataTable subclass for " + "the %s attribute on %s." % (attr_name, self.__class__.__name__)) def set_tables(self, tables): - if tables: - self.navigation_table = tables.get(self.navigation_table_class - ._meta.name, None) - self.content_table = tables.get(self.content_table_class - ._meta.name, None) - else: - raise ValueError("There are no tables passed to class %s." % - self.__class__.__name__) + """ + Sets the table instances on the browser from a dictionary mapping table + names to table instances (as constructed by MultiTableView). + """ + self.navigation_table = tables[self.navigation_table_class._meta.name] + self.content_table = tables[self.content_table_class._meta.name] def render(self): browser_template = template.loader.get_template(self.template) diff --git a/horizon/browsers/views.py b/horizon/browsers/views.py index 64e548a4b..933795e10 100644 --- a/horizon/browsers/views.py +++ b/horizon/browsers/views.py @@ -16,37 +16,32 @@ from collections import defaultdict +from django.utils.translation import ugettext_lazy as _ + from horizon.tables import MultiTableView class ResourceBrowserView(MultiTableView): browser_class = None - data_method_pattern = "get_%s_data" def __init__(self, *args, **kwargs): - self.browser_class = getattr(self, "browser_class", None) if not self.browser_class: - raise ValueError("You must specify a ResourceBrowser class " - " for the browser_class attribute on %s " + raise ValueError("You must specify a ResourceBrowser subclass " + "for the browser_class attribute on %s." % self.__class__.__name__) - - self.navigation_table = self.browser_class.navigation_table_class - self.content_table = self.browser_class.content_table_class - - # Check and set up the method the view would use to collect data - self._data_methods = defaultdict(list) - self.table_classes = (self.navigation_table, self.content_table) - self.get_data_methods(self.table_classes, self._data_methods) - - self._tables = {} - self._data = {} + self.table_classes = (self.browser_class.navigation_table_class, + self.browser_class.content_table_class) + super(ResourceBrowserView, self).__init__(*args, **kwargs) + self.navigation_selection = False def get_browser(self): if not hasattr(self, "browser"): - tables = self.get_tables() - self.browser = self.browser_class(self.request, - tables, - **self.kwargs) + self.browser = self.browser_class(self.request, **self.kwargs) + self.browser.set_tables(self.get_tables()) + if not self.navigation_selection: + ct = self.browser.content_table + item = self.browser.navigable_item_name.lower() + ct._no_data_message = _("Select a %s to browse.") % item return self.browser def get_context_data(self, **kwargs): diff --git a/horizon/dashboards/nova/containers/browsers.py b/horizon/dashboards/nova/containers/browsers.py index 0b986a8c0..68aab4c99 100644 --- a/horizon/dashboards/nova/containers/browsers.py +++ b/horizon/dashboards/nova/containers/browsers.py @@ -30,3 +30,4 @@ class ContainerBrowser(browsers.ResourceBrowser): verbose_name = _("Swift") navigation_table_class = ContainersTable content_table_class = ObjectsTable + navigable_item_name = _("Container") diff --git a/horizon/dashboards/nova/containers/forms.py b/horizon/dashboards/nova/containers/forms.py index bde2f32c2..82a493625 100644 --- a/horizon/dashboards/nova/containers/forms.py +++ b/horizon/dashboards/nova/containers/forms.py @@ -29,6 +29,8 @@ from horizon import exceptions from horizon import forms from horizon import messages +from .tables import wrap_delimiter + LOG = logging.getLogger(__name__) @@ -90,8 +92,6 @@ class UploadObject(forms.SelfHandlingForm): data['container_name'], object_path, object_file) - obj.metadata['orig-filename'] = object_file.name - obj.sync_metadata() messages.success(request, _("Object was successfully uploaded.")) return obj except: @@ -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:index" + index = "horizon:nova:containers:index" orig_container = data['orig_container_name'] orig_object = data['orig_object_name'] new_container = data['new_container_name'] @@ -124,14 +124,15 @@ class CopyObject(forms.SelfHandlingForm): # Iteratively make sure all the directory markers exist. if data['path']: path_component = "" - for bit in data['path'].split("/"): + for bit in [i for i in data['path'].split("/") if i]: path_component += bit try: api.swift.swift_create_subfolder(request, new_container, path_component) except: - redirect = reverse(object_index, args=(orig_container,)) + redirect = reverse(index, + args=(wrap_delimiter(orig_container),)) exceptions.handle(request, _("Unable to copy object."), redirect=redirect) @@ -154,10 +155,10 @@ class CopyObject(forms.SelfHandlingForm): return True except exceptions.HorizonException, exc: messages.error(request, exc) - raise exceptions.Http302(reverse(object_index, - args=[orig_container])) + raise exceptions.Http302(reverse(index, + args=[wrap_delimiter(orig_container)])) except: - redirect = reverse(object_index, args=(orig_container,)) + redirect = reverse(index, args=[wrap_delimiter(orig_container)]) exceptions.handle(request, _("Unable to copy object."), redirect=redirect) diff --git a/horizon/dashboards/nova/containers/tables.py b/horizon/dashboards/nova/containers/tables.py index 65b2db351..6799a0eb9 100644 --- a/horizon/dashboards/nova/containers/tables.py +++ b/horizon/dashboards/nova/containers/tables.py @@ -16,24 +16,23 @@ import logging -from cloudfiles.errors import ContainerNotEmpty from django.core.urlresolvers import reverse from django.template.defaultfilters import filesizeformat from django.utils import http 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 + if not name.endswith(FOLDER_DELIMITER): + return name + FOLDER_DELIMITER + return name class DeleteContainer(tables.DeleteAction): @@ -42,12 +41,7 @@ class DeleteContainer(tables.DeleteAction): completion_url = "horizon:nova:containers:index" def delete(self, request, obj_id): - try: - api.swift_delete_container(request, obj_id) - except ContainerNotEmpty: - messages.error(request, - _('Containers must be empty before deletion.')) - raise + api.swift_delete_container(request, obj_id) def get_success_url(self, request=None): """ @@ -112,7 +106,7 @@ class UploadObject(tables.LinkAction): def get_size_used(container): - return filesizeformat(container.size_used) + return filesizeformat(container.bytes) def get_container_link(container): @@ -121,7 +115,8 @@ def get_container_link(container): class ContainersTable(tables.DataTable): - name = tables.Column("name", link=get_container_link, + name = tables.Column("name", + link=get_container_link, verbose_name=_("Container Name")) def get_object_id(self, container): @@ -131,8 +126,9 @@ class ContainersTable(tables.DataTable): name = "containers" verbose_name = _("Containers") table_actions = (CreateContainer,) - row_actions = (ListObjects, UploadObject, DeleteContainer) + row_actions = (DeleteContainer,) browser_table = "navigation" + footer = False class DeleteObject(tables.DeleteAction): @@ -143,7 +139,7 @@ class DeleteObject(tables.DeleteAction): def delete(self, request, obj_id): obj = self.table.get_object_by_id(obj_id) - container_name = obj.container.name + container_name = obj.container_name api.swift_delete_object(request, container_name, obj_id) @@ -169,7 +165,8 @@ class CopyObject(tables.LinkAction): allowed_data_types = ("objects",) def get_link_url(self, obj): - return reverse(self.url, args=(http.urlquote(obj.container.name), + container_name = self.table.kwargs['container_name'] + return reverse(self.url, args=(http.urlquote(container_name), http.urlquote(obj.name))) @@ -181,20 +178,21 @@ class DownloadObject(tables.LinkAction): allowed_data_types = ("objects",) def get_link_url(self, obj): - return reverse(self.url, args=(http.urlquote(obj.container.name), + container_name = self.table.kwargs['container_name'] + return reverse(self.url, args=(http.urlquote(container_name), http.urlquote(obj.name))) class ObjectFilterAction(tables.FilterAction): def _filtered_data(self, table, filter_string): - request = table._meta.request + request = table.request container = self.table.kwargs['container_name'] subfolder = self.table.kwargs['subfolder_path'] - path = subfolder + FOLDER_DELIMITER if subfolder else '' + prefix = wrap_delimiter(subfolder) if subfolder else '' self.filtered_data = api.swift_filter_objects(request, filter_string, container, - path=path) + prefix=prefix) return self.filtered_data def filter_subfolders_data(self, table, objects, filter_string): @@ -218,11 +216,12 @@ def sanitize_name(name): def get_size(obj): - return filesizeformat(obj.size) + if obj.bytes: + return filesizeformat(obj.bytes) def get_link_subfolder(subfolder): - container_name = subfolder.container.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)))) @@ -248,10 +247,10 @@ class CreateSubfolder(CreateContainer): class ObjectsTable(tables.DataTable): name = tables.Column("name", - link=get_link_subfolder, - allowed_data_types=("subfolders",), - verbose_name=_("Object Name"), - filters=(sanitize_name,)) + link=get_link_subfolder, + allowed_data_types=("subfolders",), + verbose_name=_("Object Name"), + filters=(sanitize_name,)) size = tables.Column(get_size, verbose_name=_('Size')) @@ -267,3 +266,4 @@ class ObjectsTable(tables.DataTable): DeleteSubfolder) data_types = ("subfolders", "objects") browser_table = "content" + footer = False diff --git a/horizon/dashboards/nova/containers/tests.py b/horizon/dashboards/nova/containers/tests.py index a39ac2209..e3f67af4d 100644 --- a/horizon/dashboards/nova/containers/tests.py +++ b/horizon/dashboards/nova/containers/tests.py @@ -20,7 +20,6 @@ import tempfile -from cloudfiles.errors import ContainerNotEmpty from django import http from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.urlresolvers import reverse @@ -35,8 +34,8 @@ from . import forms CONTAINER_INDEX_URL = reverse('horizon:nova:containers:index') -class ContainerViewTests(test.TestCase): - def test_index(self): +class SwiftTests(test.TestCase): + def test_index_no_container_selected(self): containers = self.containers.list() self.mox.StubOutWithMock(api, 'swift_get_containers') api.swift_get_containers(IsA(http.HttpRequest), marker=None) \ @@ -66,7 +65,7 @@ class ContainerViewTests(test.TestCase): def test_delete_container_nonempty(self): container = self.containers.first() self.mox.StubOutWithMock(api, 'swift_delete_container') - exc = ContainerNotEmpty('containerNotEmpty') + exc = self.exceptions.swift exc.silence_logging = True api.swift_delete_container(IsA(http.HttpRequest), container.name).AndRaise(exc) @@ -97,9 +96,7 @@ class ContainerViewTests(test.TestCase): args=[wrap_delimiter(self.containers.first().name)]) self.assertRedirectsNoFollow(res, url) - -class IndexViewTests(test.TestCase): - def test_index(self): + def test_index_container_selected(self): self.mox.StubOutWithMock(api, 'swift_get_containers') self.mox.StubOutWithMock(api, 'swift_get_objects') containers = (self.containers.list(), False) @@ -109,7 +106,7 @@ class IndexViewTests(test.TestCase): api.swift_get_objects(IsA(http.HttpRequest), self.containers.first().name, marker=None, - path=None).AndReturn(ret) + prefix=None).AndReturn(ret) self.mox.ReplayAll() res = self.client.get(reverse('horizon:nova:containers:index', @@ -123,11 +120,6 @@ class IndexViewTests(test.TestCase): expected, lambda obj: obj.name.encode('utf8')) - def test_upload_index(self): - res = self.client.get(reverse('horizon:nova:containers:object_upload', - args=[self.containers.first().name])) - self.assertTemplateUsed(res, 'nova/containers/upload.html') - def test_upload(self): container = self.containers.first() obj = self.objects.first() @@ -143,11 +135,14 @@ class IndexViewTests(test.TestCase): container.name, obj.name, IsA(InMemoryUploadedFile)).AndReturn(obj) - self.mox.StubOutWithMock(obj, 'sync_metadata') - obj.sync_metadata() self.mox.ReplayAll() + upload_url = reverse('horizon:nova:containers:object_upload', args=[container.name]) + + res = self.client.get(upload_url) + self.assertTemplateUsed(res, 'nova/containers/upload.html') + res = self.client.get(upload_url) self.assertContains(res, 'enctype="multipart/form-data"') @@ -167,13 +162,6 @@ class IndexViewTests(test.TestCase): self.assertNoMessages() self.assertContains(res, "Slash is not an allowed character.") - # Test invalid container name - #formData['container_name'] = "contains/a/slash" - #formData['name'] = "no_slash" - #res = self.client.post(upload_url, formData) - #self.assertNoMessages() - #self.assertContains(res, "Slash is not an allowed character.") - def test_delete(self): container = self.containers.first() obj = self.objects.first() @@ -196,22 +184,17 @@ class IndexViewTests(test.TestCase): def test_download(self): container = self.containers.first() obj = self.objects.first() - OBJECT_DATA = 'objectData' - self.mox.StubOutWithMock(api, 'swift_get_object_data') self.mox.StubOutWithMock(api.swift, 'swift_get_object') api.swift.swift_get_object(IsA(http.HttpRequest), container.name, obj.name).AndReturn(obj) - api.swift_get_object_data(IsA(http.HttpRequest), - container.name, - obj.name).AndReturn(OBJECT_DATA) self.mox.ReplayAll() download_url = reverse('horizon:nova:containers:object_download', args=[container.name, obj.name]) res = self.client.get(download_url) - self.assertEqual(res.content, OBJECT_DATA) + self.assertEqual(res.content, obj.data) self.assertTrue(res.has_header('Content-Disposition')) def test_copy_index(self): diff --git a/horizon/dashboards/nova/containers/views.py b/horizon/dashboards/nova/containers/views.py index e2d5d9a8d..d262cec31 100644 --- a/horizon/dashboards/nova/containers/views.py +++ b/horizon/dashboards/nova/containers/views.py @@ -57,23 +57,24 @@ class ContainerView(browsers.ResourceBrowserView): def objects(self): """ Returns a list of objects given the subfolder's path. - The path is from the kwargs of the request + The path is from the kwargs of the request. """ if not hasattr(self, "_objects"): objects = [] self._more = None marker = self.request.GET.get('marker', None) container_name = self.kwargs['container_name'] - subfolders = self.kwargs['subfolder_path'] + subfolder = self.kwargs['subfolder_path'] prefix = None if container_name: - if subfolders: - prefix = subfolders.rstrip(FOLDER_DELIMITER) + self.navigation_selection = True + if subfolder: + prefix = subfolder try: objects, self._more = api.swift_get_objects(self.request, container_name, marker=marker, - path=prefix) + prefix=prefix) except: self._more = None objects = [] @@ -82,21 +83,19 @@ class ContainerView(browsers.ResourceBrowserView): self._objects = objects return self._objects - def get_objects_data(self): - """ Returns the objects within the in the current folder. + def is_subdir(self, item): + return getattr(item, "content_type", None) == "application/directory" - These objects are those whose names don't contain '/' after - striped the path out - """ - filtered_objects = [item for item in self.objects if - item.content_type != "application/directory"] + def get_objects_data(self): + """ Returns a list of objects within the current folder. """ + filtered_objects = [item for item in self.objects + if not self.is_subdir(item)] return filtered_objects def get_subfolders_data(self): - """ Returns a list of subfolders given the current folder path. - """ - filtered_objects = [item for item in self.objects if - item.content_type == "application/directory"] + """ Returns a list of subfolders within the current folder. """ + filtered_objects = [item for item in self.objects + if self.is_subdir(item)] return filtered_objects def get_context_data(self, **kwargs): @@ -158,28 +157,24 @@ class UploadView(forms.ModalFormView): 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(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) try: - object_data = api.swift_get_object_data(request, - container_name, - object_path) + obj = api.swift.swift_get_object(request, container_name, object_path) except: redirect = reverse("horizon:nova:containers:index") exceptions.handle(request, _("Unable to retrieve object."), redirect=redirect) + # Add the original file extension back on if it wasn't preserved in the + # name given to the object. + filename = object_path.rsplit(FOLDER_DELIMITER)[-1] + if not os.path.splitext(obj.name)[1] and obj.orig_name: + name, ext = os.path.splitext(obj.orig_name) + filename = "%s%s" % (filename, ext) response = http.HttpResponse() safe_name = filename.replace(",", "").encode('utf-8') response['Content-Disposition'] = 'attachment; filename=%s' % safe_name response['Content-Type'] = 'application/octet-stream' - for data in object_data: - response.write(data) + response.write(obj.data) return response diff --git a/horizon/dashboards/nova/volumes/tables.py b/horizon/dashboards/nova/volumes/tables.py index 59239d02b..996a13ecd 100644 --- a/horizon/dashboards/nova/volumes/tables.py +++ b/horizon/dashboards/nova/volumes/tables.py @@ -110,7 +110,7 @@ class AttachmentColumn(tables.Column): for a volume instance. """ def get_raw_data(self, volume): - request = self.table._meta.request + request = self.table.request link = _('Attached to %(instance)s on %(dev)s') attachments = [] # Filter out "empty" attachments which the client returns... @@ -188,7 +188,7 @@ class AttachedInstanceColumn(tables.Column): for a volume instance. """ def get_raw_data(self, attachment): - request = self.table._meta.request + request = self.table.request return safestring.mark_safe(get_attachment_name(request, attachment)) @@ -201,7 +201,7 @@ class AttachmentsTable(tables.DataTable): return obj['id'] def get_object_display(self, attachment): - instance_name = get_attachment_name(self._meta.request, attachment) + instance_name = get_attachment_name(self.request, attachment) vals = {"dev": attachment['device'], "instance_name": strip_tags(instance_name)} return _("%(dev)s on instance %(instance_name)s") % vals diff --git a/horizon/dashboards/syspanel/projects/tables.py b/horizon/dashboards/syspanel/projects/tables.py index ddc2ea8ce..a5f5ba431 100644 --- a/horizon/dashboards/syspanel/projects/tables.py +++ b/horizon/dashboards/syspanel/projects/tables.py @@ -114,7 +114,7 @@ class RemoveUserAction(tables.BatchAction): class ProjectUserRolesColumn(tables.Column): def get_raw_data(self, user): - request = self.table._meta.request + request = self.table.request try: roles = api.keystone.roles_for_user(request, user.id, diff --git a/horizon/tables/actions.py b/horizon/tables/actions.py index 57b01beef..2a643f385 100644 --- a/horizon/tables/actions.py +++ b/horizon/tables/actions.py @@ -357,12 +357,11 @@ class FilterAction(BaseAction): def assign_type_string(self, table, data, type_string): for datum in data: - setattr(datum, table._meta.data_type_name, - type_string) + setattr(datum, table._meta.data_type_name, type_string) def data_type_filter(self, table, data, filter_string): filtered_data = [] - for data_type in table._meta.data_types: + for data_type in table.data_types: func_name = "filter_%s_data" % data_type filter_func = getattr(self, func_name, None) if not filter_func and not callable(filter_func): diff --git a/horizon/tables/base.py b/horizon/tables/base.py index 49917465b..ce9404194 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -701,6 +701,11 @@ class DataTableOptions(object): The name of an attribute to assign to data passed to the table when it accepts mix data. Default: ``"_table_data_type"`` + + .. attribute:: footer + + Boolean to control whether or not to show the table's footer. + Default: ``True``. """ def __init__(self, options): self.name = getattr(options, 'name', self.__class__.__name__) @@ -715,6 +720,10 @@ class DataTableOptions(object): self.column_class = getattr(options, 'column_class', Column) self.pagination_param = getattr(options, 'pagination_param', 'marker') self.browser_table = getattr(options, 'browser_table', None) + self.footer = getattr(options, 'footer', True) + self.no_data_message = getattr(options, + "no_data_message", + _("No items to display.")) # Set self.filter if we have any FilterActions filter_actions = [action for action in self.table_actions if @@ -762,7 +771,8 @@ class DataTableOptions(object): "data_types should has more than one types" % self.name) - self.data_type_name = getattr(options, 'data_type_name', + self.data_type_name = getattr(options, + 'data_type_name', "_table_data_type") @@ -776,12 +786,12 @@ class DataTableMetaclass(type): # Gather columns; this prevents the column from being an attribute # on the DataTable class and avoids naming conflicts. columns = [] - for name, obj in attrs.items(): + for attr_name, obj in attrs.items(): if issubclass(type(obj), (opts.column_class, Column)): - column_instance = attrs.pop(name) - column_instance.name = name + column_instance = attrs.pop(attr_name) + column_instance.name = attr_name column_instance.classes.append('normal_column') - columns.append((name, column_instance)) + columns.append((attr_name, column_instance)) columns.sort(key=lambda x: x[1].creation_counter) # Iterate in reverse to preserve final order @@ -866,10 +876,11 @@ class DataTable(object): __metaclass__ = DataTableMetaclass def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs): - self._meta.request = request - self._meta.data = data + self.request = request + self.data = data self.kwargs = kwargs self._needs_form_wrapper = needs_form_wrapper + self._no_data_message = self._meta.no_data_message # Create a new set columns = [] @@ -891,19 +902,15 @@ class DataTable(object): return unicode(self._meta.verbose_name) def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__, self.name) + return '<%s: %s>' % (self.__class__.__name__, self._meta.name) @property def name(self): return self._meta.name @property - def data(self): - return self._meta.data - - @data.setter - def data(self, data): - self._meta.data = data + def footer(self): + return self._meta.footer @property def multi_select(self): @@ -916,7 +923,7 @@ class DataTable(object): if self._meta.filter and self._meta._filter_action: action = self._meta._filter_action filter_string = self.get_filter_string() - request_method = self._meta.request.method + request_method = self.request.method if filter_string and request_method == action.method: if self._meta.mixed_data_type: self._filtered_data = action.data_type_filter(self, @@ -931,7 +938,7 @@ class DataTable(object): def get_filter_string(self): filter_action = self._meta._filter_action param_name = filter_action.get_param_name() - filter_string = self._meta.request.POST.get(param_name, '') + filter_string = self.request.POST.get(param_name, '') return filter_string def _populate_data_cache(self): @@ -960,7 +967,7 @@ class DataTable(object): """ Renders the table using the template from the table options. """ table_template = template.loader.get_template(self._meta.template) extra_context = {self._meta.context_var_name: self} - context = template.RequestContext(self._meta.request, extra_context) + context = template.RequestContext(self.request, extra_context) return table_template.render(context) def get_absolute_url(self): @@ -974,11 +981,11 @@ class DataTable(object): ``request.get_full_path()`` with any query string stripped off, e.g. the path at which the table was requested. """ - return self._meta.request.get_full_path().partition('?')[0] + return self.request.get_full_path().partition('?')[0] def get_empty_message(self): """ Returns the message to be displayed when there is no data. """ - return _("No items to display.") + return self._no_data_message def get_object_by_id(self, lookup): """ @@ -1026,7 +1033,7 @@ class DataTable(object): bound_actions = [self.base_actions[action.name] for action in self._meta.table_actions] return [action for action in bound_actions if - self._filter_action(action, self._meta.request)] + self._filter_action(action, self.request)] def get_row_actions(self, datum): """ Returns a list of the action instances for a specific row. """ @@ -1038,11 +1045,11 @@ class DataTable(object): bound_action.datum = datum # Remove disallowed actions. if not self._filter_action(bound_action, - self._meta.request, + self.request, datum): continue # Hook for modifying actions based on data. No-op by default. - bound_action.update(self._meta.request, datum) + bound_action.update(self.request, datum) # Pre-create the URL for this link with appropriate parameters if issubclass(bound_action.__class__, LinkAction): bound_action.bound_url = bound_action.get_link_url(datum) @@ -1056,9 +1063,9 @@ class DataTable(object): bound_actions = self.get_table_actions() extra_context = {"table_actions": bound_actions} if self._meta.filter and \ - self._filter_action(self._meta._filter_action, self._meta.request): + self._filter_action(self._meta._filter_action, self.request): extra_context["filter"] = self._meta._filter_action - context = template.RequestContext(self._meta.request, extra_context) + context = template.RequestContext(self.request, extra_context) return table_actions_template.render(context) def render_row_actions(self, datum): @@ -1070,7 +1077,7 @@ class DataTable(object): bound_actions = self.get_row_actions(datum) extra_context = {"row_actions": bound_actions, "row_id": self.get_object_id(datum)} - context = template.RequestContext(self._meta.request, extra_context) + context = template.RequestContext(self.request, extra_context) return row_actions_template.render(context) @staticmethod @@ -1100,9 +1107,9 @@ class DataTable(object): if unsuccessful. """ # See if we have a list of ids - obj_ids = obj_ids or self._meta.request.POST.getlist('object_ids') + obj_ids = obj_ids or self.request.POST.getlist('object_ids') action = self.base_actions.get(action_name, None) - if not action or action.method != self._meta.request.method: + if not action or action.method != self.request.method: # We either didn't get an action or we're being hacked. Goodbye. return None @@ -1114,17 +1121,17 @@ class DataTable(object): obj_ids = [self.sanitize_id(i) for i in obj_ids] # Single handling is easy if not action.handles_multiple: - response = action.single(self, self._meta.request, obj_id) + response = action.single(self, self.request, obj_id) # Otherwise figure out what to pass along else: # Preference given to a specific id, since that implies # the user selected an action for just one row. if obj_id: obj_ids = [obj_id] - response = action.multiple(self, self._meta.request, obj_ids) + response = action.multiple(self, self.request, obj_ids) return response elif action and action.requires_input and not (obj_id or obj_ids): - messages.info(self._meta.request, + messages.info(self.request, _("Please select a row before taking that action.")) return None @@ -1146,7 +1153,7 @@ class DataTable(object): Determine whether the request should be handled by a preemptive action on this table or by an AJAX row update before loading any data. """ - request = self._meta.request + request = self.request table_name, action_name, obj_id = self.check_handler(request) if table_name == self.name: @@ -1181,7 +1188,7 @@ class DataTable(object): Determine whether the request should be handled by any action on this table after data has been loaded. """ - request = self._meta.request + request = self.request table_name, action_name, obj_id = self.check_handler(request) if table_name == self.name and action_name: return self.take_action(action_name, obj_id) diff --git a/horizon/tables/views.py b/horizon/tables/views.py index 835e51089..df345c39a 100644 --- a/horizon/tables/views.py +++ b/horizon/tables/views.py @@ -224,7 +224,7 @@ class MixedDataTableView(DataTableView): if not self._data: table = self.table_class self._data = {table._meta.name: []} - for data_type in table._meta.data_types: + for data_type in table.data_types: func_name = "get_%s_data" % data_type data_func = getattr(self, func_name, None) if data_func is None: @@ -239,7 +239,7 @@ class MixedDataTableView(DataTableView): def assign_type_string(self, data, type_string): for datum in data: - setattr(datum, self.table_class._meta.data_type_name, + setattr(datum, self.table_class.data_type_name, type_string) def get_table(self): diff --git a/horizon/templates/horizon/common/_data_table.html b/horizon/templates/horizon/common/_data_table.html index 34c4dd970..ccbdbc4b3 100644 --- a/horizon/templates/horizon/common/_data_table.html +++ b/horizon/templates/horizon/common/_data_table.html @@ -28,6 +28,7 @@ {% endfor %} + {% if table.footer %} {% if table.needs_summary_row %} @@ -50,6 +51,7 @@ + {% endif %} {% endwith %} {% if needs_form_wrapper %}{% endif %} diff --git a/horizon/templates/horizon/common/_resource_browser.html b/horizon/templates/horizon/common/_resource_browser.html index 6242f3eec..5caa4b880 100644 --- a/horizon/templates/horizon/common/_resource_browser.html +++ b/horizon/templates/horizon/common/_resource_browser.html @@ -1,9 +1,13 @@ {% load i18n %} -
+
{{ browser.content_table.render }}
+
+ {% blocktrans count nav_items=browser.navigation_table.data|length %}Displaying {{ nav_items }} item{% plural %}Displaying {{ nav_items }} items{% endblocktrans %} + {% blocktrans count content_items=browser.content_table.data|length %}Displaying {{ content_items }} item{% plural %}Displaying {{ content_items }} items{% endblocktrans %} +
diff --git a/horizon/test.py b/horizon/test.py index 1b294eb92..f2977b1f8 100644 --- a/horizon/test.py +++ b/horizon/test.py @@ -21,7 +21,6 @@ from functools import wraps import os -import cloudfiles as swift_client from django import http from django import test as django_test @@ -36,6 +35,8 @@ import glanceclient from keystoneclient.v2_0 import client as keystone_client from novaclient.v1_1 import client as nova_client from quantumclient.v2_0 import client as quantum_client +from swiftclient import client as swift_client + from selenium.webdriver.firefox.webdriver import WebDriver import httplib2 @@ -335,7 +336,12 @@ class APITestCase(TestCase): self.mox.StubOutWithMock(swift_client, 'Connection') self.swiftclient = self.mox.CreateMock(swift_client.Connection) while expected_calls: - swift_client.Connection(auth=mox.IgnoreArg())\ + swift_client.Connection(None, + mox.IgnoreArg(), + None, + preauthtoken=mox.IgnoreArg(), + preauthurl=mox.IgnoreArg(), + auth_version="2.0") \ .AndReturn(self.swiftclient) expected_calls -= 1 return self.swiftclient diff --git a/horizon/tests/api_tests/swift_tests.py b/horizon/tests/api_tests/swift_tests.py index 97b64e33e..6fa48f556 100644 --- a/horizon/tests/api_tests/swift_tests.py +++ b/horizon/tests/api_tests/swift_tests.py @@ -20,7 +20,7 @@ from __future__ import absolute_import -import cloudfiles +from mox import IsA from horizon import api from horizon import exceptions @@ -30,30 +30,32 @@ from horizon import test class SwiftApiTests(test.APITestCase): def test_swift_get_containers(self): containers = self.containers.list() + cont_data = [c._apidict for c in containers] swift_api = self.stub_swiftclient() - swift_api.get_all_containers(limit=1001, - marker=None).AndReturn(containers) + swift_api.get_account(limit=1001, + marker=None, + full_listing=True).AndReturn([{}, cont_data]) self.mox.ReplayAll() (conts, more) = api.swift_get_containers(self.request) self.assertEqual(len(conts), len(containers)) self.assertFalse(more) - def test_swift_create_container(self): + def test_swift_create_duplicate_container(self): container = self.containers.first() swift_api = self.stub_swiftclient(expected_calls=2) # Check for existence, then create - exc = cloudfiles.errors.NoSuchContainer() - swift_api.get_container(container.name).AndRaise(exc) - swift_api.create_container(container.name).AndReturn(container) + exc = self.exceptions.swift + swift_api.head_container(container.name).AndRaise(exc) + swift_api.put_container(container.name).AndReturn(container) self.mox.ReplayAll() # Verification handled by mox, no assertions needed. api.swift_create_container(self.request, container.name) - def test_swift_create_duplicate_container(self): + def test_swift_create_container(self): container = self.containers.first() swift_api = self.stub_swiftclient() - swift_api.get_container(container.name).AndReturn(container) + swift_api.head_container(container.name).AndReturn(container) self.mox.ReplayAll() # Verification handled by mox, no assertions needed. with self.assertRaises(exceptions.AlreadyExists): @@ -64,145 +66,55 @@ class SwiftApiTests(test.APITestCase): objects = self.objects.list() swift_api = self.stub_swiftclient() - swift_api.get_container(container.name).AndReturn(container) - self.mox.StubOutWithMock(container, 'get_objects') - container.get_objects(limit=1001, - marker=None, - prefix=None, - delimiter='/', - path=None).AndReturn(objects) + swift_api.get_container(container.name, + limit=1001, + marker=None, + prefix=None, + delimiter='/', + full_listing=True).AndReturn([{}, objects]) self.mox.ReplayAll() (objs, more) = api.swift_get_objects(self.request, container.name) self.assertEqual(len(objs), len(objects)) self.assertFalse(more) - def test_swift_filter_objects(self): - container = self.containers.first() - objects = self.objects.list() - first_obj = self.objects.first() - expected_objs = [obj.name.encode('utf8') for obj in - self.objects.filter(name=first_obj.name)] - - swift_api = self.stub_swiftclient() - swift_api.get_container(container.name).AndReturn(container) - self.mox.StubOutWithMock(container, 'get_objects') - container.get_objects(limit=10000, - marker=None, - prefix=None, - delimiter='/', - path=None).AndReturn(objects) - self.mox.ReplayAll() - - result_objs = api.swift_filter_objects(self.request, - first_obj.name, - container.name) - self.assertQuerysetEqual(result_objs, expected_objs, - lambda obj: obj.name.encode('utf8')) - def test_swift_upload_object(self): container = self.containers.first() obj = self.objects.first() - OBJECT_DATA = 'someData' + fake_name = 'fake_object.jpg' + + class FakeFile(object): + def __init__(self): + self.name = fake_name + self.data = obj.data + self.size = len(obj.data) + + headers = {'X-Object-Meta-Orig-Filename': fake_name} swift_api = self.stub_swiftclient() - swift_api.get_container(container.name).AndReturn(container) - self.mox.StubOutWithMock(container, 'create_object') - container.create_object(obj.name).AndReturn(obj) - self.mox.StubOutWithMock(obj, 'send') - obj.send(OBJECT_DATA).AndReturn(obj) + swift_api.put_object(container.name, + obj.name, + IsA(FakeFile), + headers=headers) self.mox.ReplayAll() - ret_val = api.swift_upload_object(self.request, - container.name, - obj.name, - OBJECT_DATA) - self.assertEqual(ret_val, obj) - - def test_swift_delete_object(self): - container = self.containers.first() - obj = self.objects.first() - - swift_api = self.stub_swiftclient() - swift_api.get_container(container.name).AndReturn(container) - self.mox.StubOutWithMock(container, 'delete_object') - container.delete_object(obj.name).AndReturn(obj) - self.mox.ReplayAll() - - ret_val = api.swift_delete_object(self.request, - container.name, - obj.name) - - self.assertIsNone(ret_val) - - def test_swift_get_object_data(self): - container = self.containers.first() - obj = self.objects.first() - OBJECT_DATA = 'objectData' - - swift_api = self.stub_swiftclient() - swift_api.get_container(container.name).AndReturn(container) - self.mox.StubOutWithMock(container, 'get_object') - container.get_object(obj.name).AndReturn(obj) - self.mox.StubOutWithMock(obj, 'stream') - obj.stream().AndReturn(OBJECT_DATA) - self.mox.ReplayAll() - - ret_val = api.swift_get_object_data(self.request, - container.name, - obj.name) - self.assertEqual(ret_val, OBJECT_DATA) + api.swift_upload_object(self.request, + container.name, + obj.name, + FakeFile()) def test_swift_object_exists(self): container = self.containers.first() obj = self.objects.first() swift_api = self.stub_swiftclient(expected_calls=2) - self.mox.StubOutWithMock(container, 'get_object') - swift_api.get_container(container.name).AndReturn(container) - container.get_object(obj.name).AndReturn(obj) - swift_api.get_container(container.name).AndReturn(container) - exc = cloudfiles.errors.NoSuchObject() - container.get_object(obj.name).AndRaise(exc) + swift_api.head_object(container.name, obj.name).AndReturn(container) + + exc = self.exceptions.swift + swift_api.head_object(container.name, obj.name).AndRaise(exc) self.mox.ReplayAll() args = self.request, container.name, obj.name self.assertTrue(api.swift_object_exists(*args)) # Again, for a "non-existent" object self.assertFalse(api.swift_object_exists(*args)) - - def test_swift_copy_object(self): - container = self.containers.get(name=u"container_one\u6346") - container_2 = self.containers.get(name=u"container_two\u6346") - obj = self.objects.first() - - swift_api = self.stub_swiftclient() - self.mox.StubOutWithMock(api.swift, 'swift_object_exists') - self.mox.StubOutWithMock(container, 'get_object') - self.mox.StubOutWithMock(obj, 'copy_to') - # Using the non-unicode names here, see below. - swift_api.get_container("no_unicode").AndReturn(container) - api.swift.swift_object_exists(self.request, - "also no unicode", - "obj_with_no_unicode").AndReturn(False) - container.get_object("obj_with_no_unicode").AndReturn(obj) - obj.copy_to("also no unicode", "obj_with_no_unicode") - self.mox.ReplayAll() - - # Unicode fails... we'll get to a successful test in a minute - with self.assertRaises(exceptions.HorizonException): - api.swift_copy_object(self.request, - container.name, - obj.name, - container_2.name, - obj.name) - - # Verification handled by mox. No assertions needed. - container.name = "no_unicode" - container_2.name = "also no unicode" - obj.name = "obj_with_no_unicode" - api.swift_copy_object(self.request, - container.name, - obj.name, - container_2.name, - obj.name) diff --git a/horizon/tests/test_data/exceptions.py b/horizon/tests/test_data/exceptions.py index ab55941bc..17bdfd848 100644 --- a/horizon/tests/test_data/exceptions.py +++ b/horizon/tests/test_data/exceptions.py @@ -16,6 +16,7 @@ import glanceclient.exc as glance_exceptions from keystoneclient import exceptions as keystone_exceptions from novaclient import exceptions as nova_exceptions from quantumclient.common import exceptions as quantum_exceptions +from swiftclient import client as swift_exceptions from .utils import TestDataContainer @@ -57,3 +58,6 @@ def data(TEST): quantum_exception = quantum_exceptions.QuantumClientException TEST.exceptions.quantum = create_stubbed_exception(quantum_exception) + + swift_exception = swift_exceptions.ClientException + TEST.exceptions.swift = create_stubbed_exception(swift_exception) diff --git a/horizon/tests/test_data/swift_data.py b/horizon/tests/test_data/swift_data.py index 394f0a560..aaa5575c3 100644 --- a/horizon/tests/test_data/swift_data.py +++ b/horizon/tests/test_data/swift_data.py @@ -12,13 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -import new - -from django import http - -from cloudfiles import container, storage_object - -from horizon.api import base +from horizon.api import swift from .utils import TestDataContainer @@ -26,20 +20,8 @@ def data(TEST): TEST.containers = TestDataContainer() TEST.objects = TestDataContainer() - request = http.HttpRequest() - request.user = TEST.user - - class FakeConnection(object): - def __init__(self): - self.cdn_enabled = False - self.uri = base.url_for(request, "object-store") - self.token = TEST.token - self.user_agent = "python-cloudfiles" - - conn = FakeConnection() - - container_1 = container.Container(conn, name=u"container_one\u6346") - container_2 = container.Container(conn, name=u"container_two\u6346") + container_1 = swift.Container(dict(name=u"container_one\u6346")) + container_2 = swift.Container(dict(name=u"container_two\u6346")) TEST.containers.add(container_1, container_2) object_dict = {"name": u"test_object\u6346", @@ -48,15 +30,10 @@ def data(TEST): "last_modified": None, "hash": u"object_hash"} obj_dicts = [object_dict] + obj_data = "Fake Data" + for obj_dict in obj_dicts: - swift_object = storage_object.Object(container_1, - object_record=obj_dict) + swift_object = swift.StorageObject(obj_dict, + container_1.name, + data=obj_data) TEST.objects.add(swift_object) - - # Override the list method to return the type of list cloudfiles does. - def get_object_result_list(self): - return storage_object.ObjectResults(container_1, - objects=obj_dicts) - - list_method = new.instancemethod(get_object_result_list, TEST.objects) - TEST.objects.list = list_method diff --git a/openstack_dashboard/exceptions.py b/openstack_dashboard/exceptions.py index 518c330d5..c025aeb34 100644 --- a/openstack_dashboard/exceptions.py +++ b/openstack_dashboard/exceptions.py @@ -18,11 +18,11 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudfiles import errors as swiftclient from glanceclient.common import exceptions as glanceclient from keystoneclient import exceptions as keystoneclient from novaclient import exceptions as novaclient from quantumclient.common import exceptions as quantumclient +from swiftclient import client as swiftclient UNAUTHORIZED = (keystoneclient.Unauthorized, @@ -31,17 +31,13 @@ UNAUTHORIZED = (keystoneclient.Unauthorized, novaclient.Forbidden, glanceclient.Unauthorized, quantumclient.Unauthorized, - quantumclient.Forbidden, - swiftclient.AuthenticationFailed, - swiftclient.AuthenticationError) + quantumclient.Forbidden) NOT_FOUND = (keystoneclient.NotFound, novaclient.NotFound, glanceclient.NotFound, quantumclient.NetworkNotFoundClient, - quantumclient.PortNotFoundClient, - swiftclient.NoSuchContainer, - swiftclient.NoSuchObject) + quantumclient.PortNotFoundClient) # NOTE(gabriel): This is very broad, and may need to be dialed in. RECOVERABLE = (keystoneclient.ClientException, @@ -57,4 +53,4 @@ RECOVERABLE = (keystoneclient.ClientException, quantumclient.PortInUseClient, quantumclient.AlreadyAttachedClient, quantumclient.StateInvalidClient, - swiftclient.Error) + swiftclient.ClientException) diff --git a/openstack_dashboard/static/bootstrap/less/variables.less b/openstack_dashboard/static/bootstrap/less/variables.less index afc463d88..9262ff3b6 100644 --- a/openstack_dashboard/static/bootstrap/less/variables.less +++ b/openstack_dashboard/static/bootstrap/less/variables.less @@ -104,24 +104,4 @@ // Fluid grid @fluidGridColumnWidth: 6.382978723%; -@fluidGridGutterWidth: 2.127659574%; - -//ResourceBrowser -@dataTableBorderWidth: 1px; -@dataTableBorderColor: #DDD; - -@multiSelectionWidth: 25px; -@actionsColumnWidth: 150px; -@actionsColumnPadding: 10px; - -@navigationColWidth: 150px; -@contentColWidth: 240px; - -@smallButtonHeight: 28px; -@tbodyHeight: (@dataTableBorderWidth + @smallButtonHeight + @actionsColumnPadding) * 10; - -@tableCellPadding: 8px; - -@contentTableWidth: @multiSelectionWidth + @contentColWidth * 2 + @actionsColumnWidth + @actionsColumnPadding * 2 + @tableCellPadding * 6 + @dataTableBorderWidth * 3; -@navigationTableWidth: (@navigationColWidth + @actionsColumnPadding + @tableCellPadding) * 2 + @dataTableBorderWidth * 3; -@browserWrapperWidth: @contentTableWidth + @navigationTableWidth; +@fluidGridGutterWidth: 2.127659574%; \ No newline at end of file diff --git a/openstack_dashboard/static/dashboard/less/horizon.less b/openstack_dashboard/static/dashboard/less/horizon.less index aca092a88..a36411758 100644 --- a/openstack_dashboard/static/dashboard/less/horizon.less +++ b/openstack_dashboard/static/dashboard/less/horizon.less @@ -528,7 +528,6 @@ table form { .table_actions { float: right; min-width: 400px; - margin-bottom: 10px; } .table_actions .table_search { @@ -1390,97 +1389,106 @@ label.log-length { float: left; } -/* ResourceBrowser style -*/ +//ResourceBrowser +@dataTableBorderWidth: 1px; +@dataTableBorderColor: #DDD; + +@actionsColumnPadding: 10px; + +@smallButtonHeight: 28px; +@tdHeight: @smallButtonHeight; + +@tableCellPadding: 8px; + +@contentTableWidth: 70%; +@navigationTableWidth: 30%; +@browserWrapperWidth: 100%; + +/* ResourceBrowser style */ #browser_wrapper { - width: @browserWrapperWidth; - > div{ - position: relative; - padding: 55px 0 32px 0; - float: left; - background-color: @grayLighter; - } - div.table_wrapper { - height: @tbodyHeight; - border-left: @dataTableBorderWidth solid @dataTableBorderColor; - border-right: @dataTableBorderWidth solid @dataTableBorderColor; - overflow-y: scroll; - overflow-x: hidden; - } - div.navigation_wrapper { + width: @browserWrapperWidth; + background-color: @grayLighter; + border: @dataTableBorderWidth solid @dataTableBorderColor; + .border-radius(4px); + .tfoot { + clear: both; + padding: 8px; + border-top: 1px solid @dataTableBorderColor; + background-color: #F1F1F1; + font-size: 11px; + line-height: 14px; + span { + display: inline-block; + &.navigation_table_count { width: @navigationTableWidth; - div.table_wrapper, - thead th.table_header { - width: @navigationTableWidth - 2px; - } - td { - background-color: whiteSmoke; - } - td.normal_column{ - width: @navigationColWidth; - min-width: @navigationColWidth; - > a { - width: @navigationColWidth; - min-width: @navigationColWidth; - } - } - tfoot td { - width: @navigationTableWidth - 2 * @dataTableBorderWidth - 2 * @tableCellPadding; - } + } } - div.content_wrapper { - width: @contentTableWidth; - div.table_wrapper, - thead th.table_header { - width: @contentTableWidth - 2px; - } - td.normal_column { - width: @contentColWidth; - min-width: @contentColWidth; - > a { - width: @contentColWidth; - min-width: @contentColWidth; - } - } - tfoot td { - width: @contentTableWidth - 2 * @dataTableBorderWidth - 2 * @tableCellPadding; - } + } + form, table{ + margin-bottom: 0; + } + .navigation_wrapper, .content_wrapper{ + position: relative; + float: left; + } + div.navigation_wrapper { + width: @navigationTableWidth; + div.table_wrapper, + thead th.table_header { + border-right: 0 none; + border-top-right-radius: 0; } - table { - thead { - position: absolute; - top: 0; - left: 0; - tr th { - border: @dataTableBorderWidth solid @dataTableBorderColor; - border-bottom: none; - background-color: @grayLighter; - } - } - td.multi_select_column, - th.multi_select_column{ - width: @multiSelectionWidth; - } - td.actions_column, - th.actions_column{ - padding :@actionsColumnPadding; - width: @actionsColumnWidth; - } - tbody { - tr td:first-child{ - border-left: none; - } - tr td:last-child { - border-right: none; - } - tr:last-child td { - border-bottom: @dataTableBorderWidth solid @dataTableBorderColor; - } - } - tfoot td{ - position: absolute; - left: 0; - bottom: 0; - } + td.normal_column{ + &:first-child { + border-left: 0 none; + } } + tfoot td { + border-right: 0 none; + border-bottom-right-radius: 0; + } + } + div.content_wrapper { + width: @contentTableWidth; + div.table_wrapper, + thead th.table_header { + border-left: 0 none; + border-top-left-radius: 0; + } + td{ + &:last-child { + border-right: 0 none; + } + } + tfoot td { + border-left: 0 none; + border-bottom-left-radius: 0; + } + } + table { + thead { + tr th { + border-bottom: none; + background-color: @grayLighter; + } + } + tbody { + tr:last-child td { + border-bottom: 1px solid #ddd; + border-radius: 0; + } + tr.empty td { + height: @tdHeight; + padding: @actionsColumnPadding; + } + } + &.table-striped { + tbody { + tr:nth-child(even) td, + tr:nth-child(even) th { + background-color: @white; + } + } + } + } } diff --git a/run_tests.sh b/run_tests.sh index cdb5743a5..3dbbbd366 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,7 +6,7 @@ set -o errexit # Increment me any time the environment should be rebuilt. # This includes dependncy changes, directory renames, etc. # Simple integer secuence: 1, 2, 3... -environment_version=26 +environment_version=27 #--------------------------------------------------------# function usage { diff --git a/tools/pip-requires b/tools/pip-requires index 8c6a82fd3..4246a56b5 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -2,11 +2,11 @@ Django>=1.4 django_compressor django_openstack_auth -python-cloudfiles python-glanceclient<2 python-keystoneclient python-novaclient python-quantumclient +python-swiftclient>1.1,<1.2 pytz # Horizon Utility Requirements