Switch to use python-swiftclient instead of cloudfiles.

This patch also resolves some thread-safety problems
with when the browser and associated tables are constructed
and where the request and data caches are stored on the table.

Also includes stylistic and UX enhancments to the swift
ResourceBrowser subclass.

Implements blueprint swiftclient.

Change-Id: I578277ff158b293ee50860528b069dc20e2136a9
This commit is contained in:
Gabriel Hurley 2012-08-12 21:27:21 -07:00
parent ee17b1588b
commit 801c2321bf
24 changed files with 460 additions and 526 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -30,3 +30,4 @@ class ContainerBrowser(browsers.ResourceBrowser):
verbose_name = _("Swift")
navigation_table_class = ContainersTable
content_table_class = ObjectsTable
navigable_item_name = _("Container")

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -28,6 +28,7 @@
</tr>
{% endfor %}
</tbody>
{% if table.footer %}
<tfoot>
{% if table.needs_summary_row %}
<tr class="summation">
@ -50,6 +51,7 @@
</td>
</tr>
</tfoot>
{% endif %}
</table>
{% endwith %}
{% if needs_form_wrapper %}</form>{% endif %}

View File

@ -1,9 +1,13 @@
{% load i18n %}
<div id="browser_wrapper">
<div id="browser_wrapper" class="pull-left">
<div class="navigation_wrapper">
{{ browser.navigation_table.render }}
</div>
<div class="content_wrapper">
{{ browser.content_table.render }}
</div>
<div class="tfoot">
<span class="navigation_table_count">{% blocktrans count nav_items=browser.navigation_table.data|length %}Displaying {{ nav_items }} item{% plural %}Displaying {{ nav_items }} items{% endblocktrans %}</span>
<span class="content_table_count">{% blocktrans count content_items=browser.content_table.data|length %}Displaying {{ content_items }} item{% plural %}Displaying {{ content_items }} items{% endblocktrans %}</span>
</div>
</div>

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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%;

View File

@ -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;
}
}
}
}
}

View File

@ -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 {

View File

@ -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