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 logging
import cloudfiles import swiftclient
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from horizon import exceptions from horizon import exceptions
from horizon.api.base import url_for from horizon.api.base import url_for, APIDictWrapper
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
FOLDER_DELIMITER = "/" FOLDER_DELIMITER = "/"
class SwiftAuthentication(object): class Container(APIDictWrapper):
""" Auth container in the format CloudFiles expects. """ pass
def __init__(self, storage_url, auth_token):
self.storage_url = storage_url
self.auth_token = auth_token
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): def swift_api(request):
endpoint = url_for(request, 'object-store') endpoint = url_for(request, 'object-store')
LOG.debug('Swift connection created using token "%s" and url "%s"' LOG.debug('Swift connection created using token "%s" and url "%s"'
% (request.user.token.id, endpoint)) % (request.user.token.id, endpoint))
auth = SwiftAuthentication(endpoint, request.user.token.id) return swiftclient.client.Connection(None,
return cloudfiles.get_connection(auth=auth) request.user.username,
None,
preauthtoken=request.user.token.id,
preauthurl=endpoint,
auth_version="2.0")
def swift_container_exists(request, container_name): def swift_container_exists(request, container_name):
try: try:
swift_api(request).get_container(container_name) swift_api(request).head_container(container_name)
return True return True
except cloudfiles.errors.NoSuchContainer: except swiftclient.client.ClientException:
return False return False
def swift_object_exists(request, container_name, object_name): def swift_object_exists(request, container_name, object_name):
container = swift_api(request).get_container(container_name)
try: try:
container.get_object(object_name) swift_api(request).head_object(container_name, object_name)
return True return True
except cloudfiles.errors.NoSuchObject: except swiftclient.client.ClientException:
return False return False
def swift_get_containers(request, marker=None): def swift_get_containers(request, marker=None):
limit = getattr(settings, 'API_RESULT_LIMIT', 1000) limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
containers = swift_api(request).get_all_containers(limit=limit + 1, headers, containers = swift_api(request).get_account(limit=limit + 1,
marker=marker) marker=marker,
if(len(containers) > limit): full_listing=True)
return (containers[0:-1], True) container_objs = [Container(c) for c in containers]
if(len(container_objs) > limit):
return (container_objs[0:-1], True)
else: else:
return (containers, False) return (container_objs, False)
def swift_create_container(request, name): def swift_create_container(request, name):
if swift_container_exists(request, name): if swift_container_exists(request, name):
raise exceptions.AlreadyExists(name, 'container') 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): def swift_delete_container(request, name):
swift_api(request).delete_container(name) swift_api(request).delete_container(name)
return True
def swift_get_objects(request, container_name, prefix=None, path=None, def swift_get_objects(request, container_name, prefix=None, marker=None,
marker=None): limit=None):
limit = getattr(settings, 'API_RESULT_LIMIT', 1000) limit = limit or getattr(settings, 'API_RESULT_LIMIT', 1000)
container = swift_api(request).get_container(container_name) kwargs = dict(prefix=prefix,
objects = container.get_objects(prefix=prefix,
marker=marker, marker=marker,
limit=limit + 1, limit=limit + 1,
delimiter=FOLDER_DELIMITER, delimiter=FOLDER_DELIMITER,
path=path) full_listing=True)
if(len(objects) > limit): headers, objects = swift_api(request).get_container(container_name,
return (objects[0:-1], True) **kwargs)
object_objs = _objectify(objects, container_name)
if(len(object_objs) > limit):
return (object_objs[0:-1], True)
else: else:
return (objects, False) return (object_objs, False)
def swift_filter_objects(request, filter_string, container_name, prefix=None, def swift_filter_objects(request, filter_string, container_name, prefix=None,
path=None, marker=None): marker=None):
#FIXME(kewu): Cloudfiles currently has no filtering API, thus the marker # 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 # 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 # getting the largest number of objects from a container and filtering
#on those objects. # based on those objects.
limit = 10000 limit = 9999
container = swift_api(request).get_container(container_name) objects = swift_get_objects(request,
objects = container.get_objects(prefix=prefix, container_name,
prefix=prefix,
marker=marker, marker=marker,
limit=limit, limit=limit)
delimiter=FOLDER_DELIMITER,
path=path)
filter_string_list = filter_string.lower().strip().split(' ') filter_string_list = filter_string.lower().strip().split(' ')
def matches_filter(obj): def matches_filter(obj):
for q in filter_string_list: for q in filter_string_list:
return wildcard_search(obj.name.lower(), q) return wildcard_search(obj.name.lower(), q)
return filter(matches_filter, objects) return filter(matches_filter, objects[0])
def wildcard_search(string, q): 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, def swift_copy_object(request, orig_container_name, orig_object_name,
new_container_name, new_object_name): new_container_name, new_object_name):
try: 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 # copy_to method, so to provide a better experience we check for
# unicode here and pre-empt with an error message rather than # unicode here and pre-empt with an error message rather than
# letting the call fail. # letting the call fail.
@ -153,42 +215,50 @@ def swift_copy_object(request, orig_container_name, orig_object_name,
except UnicodeEncodeError: except UnicodeEncodeError:
raise exceptions.HorizonException(_("Unicode is not currently " raise exceptions.HorizonException(_("Unicode is not currently "
"supported for object copy.")) "supported for object copy."))
container = swift_api(request).get_container(orig_container_name)
if swift_object_exists(request, new_container_name, new_object_name): if swift_object_exists(request, new_container_name, new_object_name):
raise exceptions.AlreadyExists(new_object_name, 'object') raise exceptions.AlreadyExists(new_object_name, 'object')
orig_obj = container.get_object(orig_object_name) headers = {"X-Copy-From": FOLDER_DELIMITER.join([orig_container_name,
return orig_obj.copy_to(new_container_name, new_object_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): def swift_create_subfolder(request, container_name, folder_name):
container = swift_api(request).get_container(container_name) headers = {'content-type': 'application/directory',
obj = container.create_object(folder_name)
obj.headers = {'content-type': 'application/directory',
'content-length': 0} 'content-length': 0}
obj.send('') etag = swift_api(request).put_object(container_name,
obj.sync_metadata() folder_name,
return obj 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): def swift_upload_object(request, container_name, object_name, object_file):
container = swift_api(request).get_container(container_name) headers = {}
obj = container.create_object(object_name) headers['X-Object-Meta-Orig-Filename'] = object_file.name
obj.send(object_file) etag = swift_api(request).put_object(container_name,
return obj 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): def swift_delete_object(request, container_name, object_name):
container = swift_api(request).get_container(container_name) swift_api(request).delete_object(container_name, object_name)
container.delete_object(object_name) return True
def swift_get_object(request, container_name, object_name): def swift_get_object(request, container_name, object_name):
container = swift_api(request).get_container(container_name) headers, data = swift_api(request).get_object(container_name, object_name)
return container.get_object(object_name) orig_name = headers.get("x-object-meta-orig-filename")
obj_info = {'name': object_name, 'bytes': len(data)}
return StorageObject(obj_info,
def swift_get_object_data(request, container_name, object_name): container_name,
container = swift_api(request).get_container(container_name) orig_name=orig_name,
return container.get_object(object_name).stream() data=data)

View File

@ -15,6 +15,7 @@
# under the License. # under the License.
from django import template from django import template
from django.utils.translation import ugettext_lazy as _
from horizon.tables import DataTable from horizon.tables import DataTable
from horizon.utils import html from horizon.utils import html
@ -32,6 +33,7 @@ class ResourceBrowser(html.HTMLElement):
A more verbose name for the browser meant for display purposes. A more verbose name for the browser meant for display purposes.
.. attribute:: navigation_table_class .. attribute:: navigation_table_class
This table displays data on the left side of the browser. This table displays data on the left side of the browser.
Set the ``navigation_table_class`` attribute with Set the ``navigation_table_class`` attribute with
the desired :class:`~horizon.tables.DataTable` class. the desired :class:`~horizon.tables.DataTable` class.
@ -39,6 +41,7 @@ class ResourceBrowser(html.HTMLElement):
``"navigation"``. ``"navigation"``.
.. attribute:: content_table_class .. attribute:: content_table_class
This table displays data on the right side of the browser. This table displays data on the right side of the browser.
Set the ``content_table_class`` attribute with Set the ``content_table_class`` attribute with
the desired :class:`~horizon.tables.DataTable` class. the desired :class:`~horizon.tables.DataTable` class.
@ -59,44 +62,35 @@ class ResourceBrowser(html.HTMLElement):
verbose_name = None verbose_name = None
navigation_table_class = None navigation_table_class = None
content_table_class = None content_table_class = None
navigable_item_name = _("Navigation Item")
template = "horizon/common/_resource_browser.html" template = "horizon/common/_resource_browser.html"
context_var_name = "browser" context_var_name = "browser"
def __init__(self, request, tables=None, attrs=None, def __init__(self, request, tables_dict=None, attrs=None, **kwargs):
**kwargs):
super(ResourceBrowser, self).__init__() super(ResourceBrowser, self).__init__()
self.name = getattr(self, "name", self.__class__.__name__) self.name = self.name or self.__class__.__name__
self.verbose_name = getattr(self, "verbose_name", self.name.title()) self.verbose_name = self.verbose_name or self.name.title()
self.request = request self.request = request
self.attrs.update(attrs or {}) self.attrs.update(attrs or {})
self.check_table_class(self.content_table_class, "content_table_class")
self.navigation_table_class = getattr(self, "navigation_table_class",
None)
self.check_table_class(self.navigation_table_class, self.check_table_class(self.navigation_table_class,
"navigation_table_class") "navigation_table_class")
if tables_dict:
self.content_table_class = getattr(self, "content_table_class", self.set_tables(tables_dict)
None)
self.check_table_class(self.content_table_class,
"content_table_class")
self.set_tables(tables)
def check_table_class(self, cls, attr_name): def check_table_class(self, cls, attr_name):
if not cls or not issubclass(cls, (DataTable, )): if not cls or not issubclass(cls, DataTable):
raise ValueError("You must specify a DataTable class for " raise ValueError("You must specify a DataTable subclass for "
"the %s attribute on %s " "the %s attribute on %s."
% (attr_name, self.__class__.__name__)) % (attr_name, self.__class__.__name__))
def set_tables(self, tables): def set_tables(self, tables):
if tables: """
self.navigation_table = tables.get(self.navigation_table_class Sets the table instances on the browser from a dictionary mapping table
._meta.name, None) names to table instances (as constructed by MultiTableView).
self.content_table = tables.get(self.content_table_class """
._meta.name, None) self.navigation_table = tables[self.navigation_table_class._meta.name]
else: self.content_table = tables[self.content_table_class._meta.name]
raise ValueError("There are no tables passed to class %s." %
self.__class__.__name__)
def render(self): def render(self):
browser_template = template.loader.get_template(self.template) browser_template = template.loader.get_template(self.template)

View File

@ -16,37 +16,32 @@
from collections import defaultdict from collections import defaultdict
from django.utils.translation import ugettext_lazy as _
from horizon.tables import MultiTableView from horizon.tables import MultiTableView
class ResourceBrowserView(MultiTableView): class ResourceBrowserView(MultiTableView):
browser_class = None browser_class = None
data_method_pattern = "get_%s_data"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.browser_class = getattr(self, "browser_class", None)
if not self.browser_class: if not self.browser_class:
raise ValueError("You must specify a ResourceBrowser class " raise ValueError("You must specify a ResourceBrowser subclass "
" for the browser_class attribute on %s " "for the browser_class attribute on %s."
% self.__class__.__name__) % self.__class__.__name__)
self.table_classes = (self.browser_class.navigation_table_class,
self.navigation_table = self.browser_class.navigation_table_class self.browser_class.content_table_class)
self.content_table = self.browser_class.content_table_class super(ResourceBrowserView, self).__init__(*args, **kwargs)
self.navigation_selection = False
# 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 = {}
def get_browser(self): def get_browser(self):
if not hasattr(self, "browser"): if not hasattr(self, "browser"):
tables = self.get_tables() self.browser = self.browser_class(self.request, **self.kwargs)
self.browser = self.browser_class(self.request, self.browser.set_tables(self.get_tables())
tables, if not self.navigation_selection:
**self.kwargs) 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 return self.browser
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):

View File

@ -30,3 +30,4 @@ class ContainerBrowser(browsers.ResourceBrowser):
verbose_name = _("Swift") verbose_name = _("Swift")
navigation_table_class = ContainersTable navigation_table_class = ContainersTable
content_table_class = ObjectsTable 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 forms
from horizon import messages from horizon import messages
from .tables import wrap_delimiter
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -90,8 +92,6 @@ class UploadObject(forms.SelfHandlingForm):
data['container_name'], data['container_name'],
object_path, object_path,
object_file) object_file)
obj.metadata['orig-filename'] = object_file.name
obj.sync_metadata()
messages.success(request, _("Object was successfully uploaded.")) messages.success(request, _("Object was successfully uploaded."))
return obj return obj
except: except:
@ -114,7 +114,7 @@ class CopyObject(forms.SelfHandlingForm):
self.fields['new_container_name'].choices = containers self.fields['new_container_name'].choices = containers
def handle(self, request, data): def handle(self, request, data):
object_index = "horizon:nova:containers:index" index = "horizon:nova:containers:index"
orig_container = data['orig_container_name'] orig_container = data['orig_container_name']
orig_object = data['orig_object_name'] orig_object = data['orig_object_name']
new_container = data['new_container_name'] new_container = data['new_container_name']
@ -124,14 +124,15 @@ class CopyObject(forms.SelfHandlingForm):
# Iteratively make sure all the directory markers exist. # Iteratively make sure all the directory markers exist.
if data['path']: if data['path']:
path_component = "" path_component = ""
for bit in data['path'].split("/"): for bit in [i for i in data['path'].split("/") if i]:
path_component += bit path_component += bit
try: try:
api.swift.swift_create_subfolder(request, api.swift.swift_create_subfolder(request,
new_container, new_container,
path_component) path_component)
except: except:
redirect = reverse(object_index, args=(orig_container,)) redirect = reverse(index,
args=(wrap_delimiter(orig_container),))
exceptions.handle(request, exceptions.handle(request,
_("Unable to copy object."), _("Unable to copy object."),
redirect=redirect) redirect=redirect)
@ -154,10 +155,10 @@ class CopyObject(forms.SelfHandlingForm):
return True return True
except exceptions.HorizonException, exc: except exceptions.HorizonException, exc:
messages.error(request, exc) messages.error(request, exc)
raise exceptions.Http302(reverse(object_index, raise exceptions.Http302(reverse(index,
args=[orig_container])) args=[wrap_delimiter(orig_container)]))
except: except:
redirect = reverse(object_index, args=(orig_container,)) redirect = reverse(index, args=[wrap_delimiter(orig_container)])
exceptions.handle(request, exceptions.handle(request,
_("Unable to copy object."), _("Unable to copy object."),
redirect=redirect) redirect=redirect)

View File

@ -16,24 +16,23 @@
import logging import logging
from cloudfiles.errors import ContainerNotEmpty
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.template.defaultfilters import filesizeformat from django.template.defaultfilters import filesizeformat
from django.utils import http from django.utils import http
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import api from horizon import api
from horizon import messages
from horizon import tables from horizon import tables
from horizon.api import FOLDER_DELIMITER from horizon.api import FOLDER_DELIMITER
from horizon.tables import DataTable
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def wrap_delimiter(name): def wrap_delimiter(name):
if not name.endswith(FOLDER_DELIMITER):
return name + FOLDER_DELIMITER return name + FOLDER_DELIMITER
return name
class DeleteContainer(tables.DeleteAction): class DeleteContainer(tables.DeleteAction):
@ -42,12 +41,7 @@ class DeleteContainer(tables.DeleteAction):
completion_url = "horizon:nova:containers:index" completion_url = "horizon:nova:containers:index"
def delete(self, request, obj_id): def delete(self, request, obj_id):
try:
api.swift_delete_container(request, obj_id) api.swift_delete_container(request, obj_id)
except ContainerNotEmpty:
messages.error(request,
_('Containers must be empty before deletion.'))
raise
def get_success_url(self, request=None): def get_success_url(self, request=None):
""" """
@ -112,7 +106,7 @@ class UploadObject(tables.LinkAction):
def get_size_used(container): def get_size_used(container):
return filesizeformat(container.size_used) return filesizeformat(container.bytes)
def get_container_link(container): def get_container_link(container):
@ -121,7 +115,8 @@ def get_container_link(container):
class ContainersTable(tables.DataTable): class ContainersTable(tables.DataTable):
name = tables.Column("name", link=get_container_link, name = tables.Column("name",
link=get_container_link,
verbose_name=_("Container Name")) verbose_name=_("Container Name"))
def get_object_id(self, container): def get_object_id(self, container):
@ -131,8 +126,9 @@ class ContainersTable(tables.DataTable):
name = "containers" name = "containers"
verbose_name = _("Containers") verbose_name = _("Containers")
table_actions = (CreateContainer,) table_actions = (CreateContainer,)
row_actions = (ListObjects, UploadObject, DeleteContainer) row_actions = (DeleteContainer,)
browser_table = "navigation" browser_table = "navigation"
footer = False
class DeleteObject(tables.DeleteAction): class DeleteObject(tables.DeleteAction):
@ -143,7 +139,7 @@ class DeleteObject(tables.DeleteAction):
def delete(self, request, obj_id): def delete(self, request, obj_id):
obj = self.table.get_object_by_id(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) api.swift_delete_object(request, container_name, obj_id)
@ -169,7 +165,8 @@ class CopyObject(tables.LinkAction):
allowed_data_types = ("objects",) allowed_data_types = ("objects",)
def get_link_url(self, obj): 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))) http.urlquote(obj.name)))
@ -181,20 +178,21 @@ class DownloadObject(tables.LinkAction):
allowed_data_types = ("objects",) allowed_data_types = ("objects",)
def get_link_url(self, obj): 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))) http.urlquote(obj.name)))
class ObjectFilterAction(tables.FilterAction): class ObjectFilterAction(tables.FilterAction):
def _filtered_data(self, table, filter_string): def _filtered_data(self, table, filter_string):
request = table._meta.request request = table.request
container = self.table.kwargs['container_name'] container = self.table.kwargs['container_name']
subfolder = self.table.kwargs['subfolder_path'] 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, self.filtered_data = api.swift_filter_objects(request,
filter_string, filter_string,
container, container,
path=path) prefix=prefix)
return self.filtered_data return self.filtered_data
def filter_subfolders_data(self, table, objects, filter_string): def filter_subfolders_data(self, table, objects, filter_string):
@ -218,11 +216,12 @@ def sanitize_name(name):
def get_size(obj): def get_size(obj):
return filesizeformat(obj.size) if obj.bytes:
return filesizeformat(obj.bytes)
def get_link_subfolder(subfolder): def get_link_subfolder(subfolder):
container_name = subfolder.container.name container_name = subfolder.container_name
return reverse("horizon:nova:containers:index", return reverse("horizon:nova:containers:index",
args=(http.urlquote(wrap_delimiter(container_name)), args=(http.urlquote(wrap_delimiter(container_name)),
http.urlquote(wrap_delimiter(subfolder.name)))) http.urlquote(wrap_delimiter(subfolder.name))))
@ -267,3 +266,4 @@ class ObjectsTable(tables.DataTable):
DeleteSubfolder) DeleteSubfolder)
data_types = ("subfolders", "objects") data_types = ("subfolders", "objects")
browser_table = "content" browser_table = "content"
footer = False

View File

@ -20,7 +20,6 @@
import tempfile import tempfile
from cloudfiles.errors import ContainerNotEmpty
from django import http from django import http
from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -35,8 +34,8 @@ from . import forms
CONTAINER_INDEX_URL = reverse('horizon:nova:containers:index') CONTAINER_INDEX_URL = reverse('horizon:nova:containers:index')
class ContainerViewTests(test.TestCase): class SwiftTests(test.TestCase):
def test_index(self): def test_index_no_container_selected(self):
containers = self.containers.list() containers = self.containers.list()
self.mox.StubOutWithMock(api, 'swift_get_containers') self.mox.StubOutWithMock(api, 'swift_get_containers')
api.swift_get_containers(IsA(http.HttpRequest), marker=None) \ api.swift_get_containers(IsA(http.HttpRequest), marker=None) \
@ -66,7 +65,7 @@ class ContainerViewTests(test.TestCase):
def test_delete_container_nonempty(self): def test_delete_container_nonempty(self):
container = self.containers.first() container = self.containers.first()
self.mox.StubOutWithMock(api, 'swift_delete_container') self.mox.StubOutWithMock(api, 'swift_delete_container')
exc = ContainerNotEmpty('containerNotEmpty') exc = self.exceptions.swift
exc.silence_logging = True exc.silence_logging = True
api.swift_delete_container(IsA(http.HttpRequest), api.swift_delete_container(IsA(http.HttpRequest),
container.name).AndRaise(exc) container.name).AndRaise(exc)
@ -97,9 +96,7 @@ class ContainerViewTests(test.TestCase):
args=[wrap_delimiter(self.containers.first().name)]) args=[wrap_delimiter(self.containers.first().name)])
self.assertRedirectsNoFollow(res, url) self.assertRedirectsNoFollow(res, url)
def test_index_container_selected(self):
class IndexViewTests(test.TestCase):
def test_index(self):
self.mox.StubOutWithMock(api, 'swift_get_containers') self.mox.StubOutWithMock(api, 'swift_get_containers')
self.mox.StubOutWithMock(api, 'swift_get_objects') self.mox.StubOutWithMock(api, 'swift_get_objects')
containers = (self.containers.list(), False) containers = (self.containers.list(), False)
@ -109,7 +106,7 @@ class IndexViewTests(test.TestCase):
api.swift_get_objects(IsA(http.HttpRequest), api.swift_get_objects(IsA(http.HttpRequest),
self.containers.first().name, self.containers.first().name,
marker=None, marker=None,
path=None).AndReturn(ret) prefix=None).AndReturn(ret)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:nova:containers:index', res = self.client.get(reverse('horizon:nova:containers:index',
@ -123,11 +120,6 @@ class IndexViewTests(test.TestCase):
expected, expected,
lambda obj: obj.name.encode('utf8')) 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): def test_upload(self):
container = self.containers.first() container = self.containers.first()
obj = self.objects.first() obj = self.objects.first()
@ -143,11 +135,14 @@ class IndexViewTests(test.TestCase):
container.name, container.name,
obj.name, obj.name,
IsA(InMemoryUploadedFile)).AndReturn(obj) IsA(InMemoryUploadedFile)).AndReturn(obj)
self.mox.StubOutWithMock(obj, 'sync_metadata')
obj.sync_metadata()
self.mox.ReplayAll() self.mox.ReplayAll()
upload_url = reverse('horizon:nova:containers:object_upload', upload_url = reverse('horizon:nova:containers:object_upload',
args=[container.name]) args=[container.name])
res = self.client.get(upload_url)
self.assertTemplateUsed(res, 'nova/containers/upload.html')
res = self.client.get(upload_url) res = self.client.get(upload_url)
self.assertContains(res, 'enctype="multipart/form-data"') self.assertContains(res, 'enctype="multipart/form-data"')
@ -167,13 +162,6 @@ class IndexViewTests(test.TestCase):
self.assertNoMessages() self.assertNoMessages()
self.assertContains(res, "Slash is not an allowed character.") 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): def test_delete(self):
container = self.containers.first() container = self.containers.first()
obj = self.objects.first() obj = self.objects.first()
@ -196,22 +184,17 @@ class IndexViewTests(test.TestCase):
def test_download(self): def test_download(self):
container = self.containers.first() container = self.containers.first()
obj = self.objects.first() obj = self.objects.first()
OBJECT_DATA = 'objectData'
self.mox.StubOutWithMock(api, 'swift_get_object_data')
self.mox.StubOutWithMock(api.swift, 'swift_get_object') self.mox.StubOutWithMock(api.swift, 'swift_get_object')
api.swift.swift_get_object(IsA(http.HttpRequest), api.swift.swift_get_object(IsA(http.HttpRequest),
container.name, container.name,
obj.name).AndReturn(obj) obj.name).AndReturn(obj)
api.swift_get_object_data(IsA(http.HttpRequest),
container.name,
obj.name).AndReturn(OBJECT_DATA)
self.mox.ReplayAll() self.mox.ReplayAll()
download_url = reverse('horizon:nova:containers:object_download', download_url = reverse('horizon:nova:containers:object_download',
args=[container.name, obj.name]) args=[container.name, obj.name])
res = self.client.get(download_url) 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')) self.assertTrue(res.has_header('Content-Disposition'))
def test_copy_index(self): def test_copy_index(self):

View File

@ -57,23 +57,24 @@ class ContainerView(browsers.ResourceBrowserView):
def objects(self): def objects(self):
""" Returns a list of objects given the subfolder's path. """ 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"): if not hasattr(self, "_objects"):
objects = [] objects = []
self._more = None self._more = None
marker = self.request.GET.get('marker', None) marker = self.request.GET.get('marker', None)
container_name = self.kwargs['container_name'] container_name = self.kwargs['container_name']
subfolders = self.kwargs['subfolder_path'] subfolder = self.kwargs['subfolder_path']
prefix = None prefix = None
if container_name: if container_name:
if subfolders: self.navigation_selection = True
prefix = subfolders.rstrip(FOLDER_DELIMITER) if subfolder:
prefix = subfolder
try: try:
objects, self._more = api.swift_get_objects(self.request, objects, self._more = api.swift_get_objects(self.request,
container_name, container_name,
marker=marker, marker=marker,
path=prefix) prefix=prefix)
except: except:
self._more = None self._more = None
objects = [] objects = []
@ -82,21 +83,19 @@ class ContainerView(browsers.ResourceBrowserView):
self._objects = objects self._objects = objects
return self._objects return self._objects
def get_objects_data(self): def is_subdir(self, item):
""" Returns the objects within the in the current folder. return getattr(item, "content_type", None) == "application/directory"
These objects are those whose names don't contain '/' after def get_objects_data(self):
striped the path out """ Returns a list of objects within the current folder. """
""" filtered_objects = [item for item in self.objects
filtered_objects = [item for item in self.objects if if not self.is_subdir(item)]
item.content_type != "application/directory"]
return filtered_objects return filtered_objects
def get_subfolders_data(self): def get_subfolders_data(self):
""" Returns a list of subfolders given the current folder path. """ Returns a list of subfolders within the current folder. """
""" filtered_objects = [item for item in self.objects
filtered_objects = [item for item in self.objects if if self.is_subdir(item)]
item.content_type == "application/directory"]
return filtered_objects return filtered_objects
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -158,28 +157,24 @@ class UploadView(forms.ModalFormView):
def object_download(request, container_name, object_path): 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: try:
object_data = api.swift_get_object_data(request, obj = api.swift.swift_get_object(request, container_name, object_path)
container_name,
object_path)
except: except:
redirect = reverse("horizon:nova:containers:index") redirect = reverse("horizon:nova:containers:index")
exceptions.handle(request, exceptions.handle(request,
_("Unable to retrieve object."), _("Unable to retrieve object."),
redirect=redirect) 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() response = http.HttpResponse()
safe_name = filename.replace(",", "").encode('utf-8') safe_name = filename.replace(",", "").encode('utf-8')
response['Content-Disposition'] = 'attachment; filename=%s' % safe_name response['Content-Disposition'] = 'attachment; filename=%s' % safe_name
response['Content-Type'] = 'application/octet-stream' response['Content-Type'] = 'application/octet-stream'
for data in object_data: response.write(obj.data)
response.write(data)
return response return response

View File

@ -110,7 +110,7 @@ class AttachmentColumn(tables.Column):
for a volume instance. for a volume instance.
""" """
def get_raw_data(self, volume): def get_raw_data(self, volume):
request = self.table._meta.request request = self.table.request
link = _('Attached to %(instance)s on %(dev)s') link = _('Attached to %(instance)s on %(dev)s')
attachments = [] attachments = []
# Filter out "empty" attachments which the client returns... # Filter out "empty" attachments which the client returns...
@ -188,7 +188,7 @@ class AttachedInstanceColumn(tables.Column):
for a volume instance. for a volume instance.
""" """
def get_raw_data(self, attachment): def get_raw_data(self, attachment):
request = self.table._meta.request request = self.table.request
return safestring.mark_safe(get_attachment_name(request, attachment)) return safestring.mark_safe(get_attachment_name(request, attachment))
@ -201,7 +201,7 @@ class AttachmentsTable(tables.DataTable):
return obj['id'] return obj['id']
def get_object_display(self, attachment): 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'], vals = {"dev": attachment['device'],
"instance_name": strip_tags(instance_name)} "instance_name": strip_tags(instance_name)}
return _("%(dev)s on instance %(instance_name)s") % vals return _("%(dev)s on instance %(instance_name)s") % vals

View File

@ -114,7 +114,7 @@ class RemoveUserAction(tables.BatchAction):
class ProjectUserRolesColumn(tables.Column): class ProjectUserRolesColumn(tables.Column):
def get_raw_data(self, user): def get_raw_data(self, user):
request = self.table._meta.request request = self.table.request
try: try:
roles = api.keystone.roles_for_user(request, roles = api.keystone.roles_for_user(request,
user.id, user.id,

View File

@ -357,12 +357,11 @@ class FilterAction(BaseAction):
def assign_type_string(self, table, data, type_string): def assign_type_string(self, table, data, type_string):
for datum in data: for datum in data:
setattr(datum, table._meta.data_type_name, setattr(datum, table._meta.data_type_name, type_string)
type_string)
def data_type_filter(self, table, data, filter_string): def data_type_filter(self, table, data, filter_string):
filtered_data = [] filtered_data = []
for data_type in table._meta.data_types: for data_type in table.data_types:
func_name = "filter_%s_data" % data_type func_name = "filter_%s_data" % data_type
filter_func = getattr(self, func_name, None) filter_func = getattr(self, func_name, None)
if not filter_func and not callable(filter_func): 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 The name of an attribute to assign to data passed to the table when it
accepts mix data. Default: ``"_table_data_type"`` 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): def __init__(self, options):
self.name = getattr(options, 'name', self.__class__.__name__) self.name = getattr(options, 'name', self.__class__.__name__)
@ -715,6 +720,10 @@ class DataTableOptions(object):
self.column_class = getattr(options, 'column_class', Column) self.column_class = getattr(options, 'column_class', Column)
self.pagination_param = getattr(options, 'pagination_param', 'marker') self.pagination_param = getattr(options, 'pagination_param', 'marker')
self.browser_table = getattr(options, 'browser_table', None) 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 # Set self.filter if we have any FilterActions
filter_actions = [action for action in self.table_actions if 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" % "data_types should has more than one types" %
self.name) self.name)
self.data_type_name = getattr(options, 'data_type_name', self.data_type_name = getattr(options,
'data_type_name',
"_table_data_type") "_table_data_type")
@ -776,12 +786,12 @@ class DataTableMetaclass(type):
# Gather columns; this prevents the column from being an attribute # Gather columns; this prevents the column from being an attribute
# on the DataTable class and avoids naming conflicts. # on the DataTable class and avoids naming conflicts.
columns = [] columns = []
for name, obj in attrs.items(): for attr_name, obj in attrs.items():
if issubclass(type(obj), (opts.column_class, Column)): if issubclass(type(obj), (opts.column_class, Column)):
column_instance = attrs.pop(name) column_instance = attrs.pop(attr_name)
column_instance.name = name column_instance.name = attr_name
column_instance.classes.append('normal_column') 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) columns.sort(key=lambda x: x[1].creation_counter)
# Iterate in reverse to preserve final order # Iterate in reverse to preserve final order
@ -866,10 +876,11 @@ class DataTable(object):
__metaclass__ = DataTableMetaclass __metaclass__ = DataTableMetaclass
def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs): def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):
self._meta.request = request self.request = request
self._meta.data = data self.data = data
self.kwargs = kwargs self.kwargs = kwargs
self._needs_form_wrapper = needs_form_wrapper self._needs_form_wrapper = needs_form_wrapper
self._no_data_message = self._meta.no_data_message
# Create a new set # Create a new set
columns = [] columns = []
@ -891,19 +902,15 @@ class DataTable(object):
return unicode(self._meta.verbose_name) return unicode(self._meta.verbose_name)
def __repr__(self): def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.name) return '<%s: %s>' % (self.__class__.__name__, self._meta.name)
@property @property
def name(self): def name(self):
return self._meta.name return self._meta.name
@property @property
def data(self): def footer(self):
return self._meta.data return self._meta.footer
@data.setter
def data(self, data):
self._meta.data = data
@property @property
def multi_select(self): def multi_select(self):
@ -916,7 +923,7 @@ class DataTable(object):
if self._meta.filter and self._meta._filter_action: if self._meta.filter and self._meta._filter_action:
action = self._meta._filter_action action = self._meta._filter_action
filter_string = self.get_filter_string() 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 filter_string and request_method == action.method:
if self._meta.mixed_data_type: if self._meta.mixed_data_type:
self._filtered_data = action.data_type_filter(self, self._filtered_data = action.data_type_filter(self,
@ -931,7 +938,7 @@ class DataTable(object):
def get_filter_string(self): def get_filter_string(self):
filter_action = self._meta._filter_action filter_action = self._meta._filter_action
param_name = filter_action.get_param_name() 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 return filter_string
def _populate_data_cache(self): def _populate_data_cache(self):
@ -960,7 +967,7 @@ class DataTable(object):
""" Renders the table using the template from the table options. """ """ Renders the table using the template from the table options. """
table_template = template.loader.get_template(self._meta.template) table_template = template.loader.get_template(self._meta.template)
extra_context = {self._meta.context_var_name: self} 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) return table_template.render(context)
def get_absolute_url(self): def get_absolute_url(self):
@ -974,11 +981,11 @@ class DataTable(object):
``request.get_full_path()`` with any query string stripped off, ``request.get_full_path()`` with any query string stripped off,
e.g. the path at which the table was requested. 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): def get_empty_message(self):
""" Returns the message to be displayed when there is no data. """ """ 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): def get_object_by_id(self, lookup):
""" """
@ -1026,7 +1033,7 @@ class DataTable(object):
bound_actions = [self.base_actions[action.name] for bound_actions = [self.base_actions[action.name] for
action in self._meta.table_actions] action in self._meta.table_actions]
return [action for action in bound_actions if 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): def get_row_actions(self, datum):
""" Returns a list of the action instances for a specific row. """ """ Returns a list of the action instances for a specific row. """
@ -1038,11 +1045,11 @@ class DataTable(object):
bound_action.datum = datum bound_action.datum = datum
# Remove disallowed actions. # Remove disallowed actions.
if not self._filter_action(bound_action, if not self._filter_action(bound_action,
self._meta.request, self.request,
datum): datum):
continue continue
# Hook for modifying actions based on data. No-op by default. # 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 # Pre-create the URL for this link with appropriate parameters
if issubclass(bound_action.__class__, LinkAction): if issubclass(bound_action.__class__, LinkAction):
bound_action.bound_url = bound_action.get_link_url(datum) bound_action.bound_url = bound_action.get_link_url(datum)
@ -1056,9 +1063,9 @@ class DataTable(object):
bound_actions = self.get_table_actions() bound_actions = self.get_table_actions()
extra_context = {"table_actions": bound_actions} extra_context = {"table_actions": bound_actions}
if self._meta.filter and \ 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 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) return table_actions_template.render(context)
def render_row_actions(self, datum): def render_row_actions(self, datum):
@ -1070,7 +1077,7 @@ class DataTable(object):
bound_actions = self.get_row_actions(datum) bound_actions = self.get_row_actions(datum)
extra_context = {"row_actions": bound_actions, extra_context = {"row_actions": bound_actions,
"row_id": self.get_object_id(datum)} "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) return row_actions_template.render(context)
@staticmethod @staticmethod
@ -1100,9 +1107,9 @@ class DataTable(object):
if unsuccessful. if unsuccessful.
""" """
# See if we have a list of ids # 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) 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. # We either didn't get an action or we're being hacked. Goodbye.
return None return None
@ -1114,17 +1121,17 @@ class DataTable(object):
obj_ids = [self.sanitize_id(i) for i in obj_ids] obj_ids = [self.sanitize_id(i) for i in obj_ids]
# Single handling is easy # Single handling is easy
if not action.handles_multiple: 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 # Otherwise figure out what to pass along
else: else:
# Preference given to a specific id, since that implies # Preference given to a specific id, since that implies
# the user selected an action for just one row. # the user selected an action for just one row.
if obj_id: if obj_id:
obj_ids = [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 return response
elif action and action.requires_input and not (obj_id or obj_ids): 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.")) _("Please select a row before taking that action."))
return None return None
@ -1146,7 +1153,7 @@ class DataTable(object):
Determine whether the request should be handled by a preemptive action Determine whether the request should be handled by a preemptive action
on this table or by an AJAX row update before loading any data. 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) table_name, action_name, obj_id = self.check_handler(request)
if table_name == self.name: if table_name == self.name:
@ -1181,7 +1188,7 @@ class DataTable(object):
Determine whether the request should be handled by any action on this Determine whether the request should be handled by any action on this
table after data has been loaded. table after data has been loaded.
""" """
request = self._meta.request request = self.request
table_name, action_name, obj_id = self.check_handler(request) table_name, action_name, obj_id = self.check_handler(request)
if table_name == self.name and action_name: if table_name == self.name and action_name:
return self.take_action(action_name, obj_id) return self.take_action(action_name, obj_id)

View File

@ -224,7 +224,7 @@ class MixedDataTableView(DataTableView):
if not self._data: if not self._data:
table = self.table_class table = self.table_class
self._data = {table._meta.name: []} 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 func_name = "get_%s_data" % data_type
data_func = getattr(self, func_name, None) data_func = getattr(self, func_name, None)
if data_func is None: if data_func is None:
@ -239,7 +239,7 @@ class MixedDataTableView(DataTableView):
def assign_type_string(self, data, type_string): def assign_type_string(self, data, type_string):
for datum in data: for datum in data:
setattr(datum, self.table_class._meta.data_type_name, setattr(datum, self.table_class.data_type_name,
type_string) type_string)
def get_table(self): def get_table(self):

View File

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

View File

@ -1,9 +1,13 @@
{% load i18n %} {% load i18n %}
<div id="browser_wrapper"> <div id="browser_wrapper" class="pull-left">
<div class="navigation_wrapper"> <div class="navigation_wrapper">
{{ browser.navigation_table.render }} {{ browser.navigation_table.render }}
</div> </div>
<div class="content_wrapper"> <div class="content_wrapper">
{{ browser.content_table.render }} {{ browser.content_table.render }}
</div> </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> </div>

View File

@ -21,7 +21,6 @@
from functools import wraps from functools import wraps
import os import os
import cloudfiles as swift_client
from django import http from django import http
from django import test as django_test from django import test as django_test
@ -36,6 +35,8 @@ import glanceclient
from keystoneclient.v2_0 import client as keystone_client from keystoneclient.v2_0 import client as keystone_client
from novaclient.v1_1 import client as nova_client from novaclient.v1_1 import client as nova_client
from quantumclient.v2_0 import client as quantum_client from quantumclient.v2_0 import client as quantum_client
from swiftclient import client as swift_client
from selenium.webdriver.firefox.webdriver import WebDriver from selenium.webdriver.firefox.webdriver import WebDriver
import httplib2 import httplib2
@ -335,7 +336,12 @@ class APITestCase(TestCase):
self.mox.StubOutWithMock(swift_client, 'Connection') self.mox.StubOutWithMock(swift_client, 'Connection')
self.swiftclient = self.mox.CreateMock(swift_client.Connection) self.swiftclient = self.mox.CreateMock(swift_client.Connection)
while expected_calls: 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) .AndReturn(self.swiftclient)
expected_calls -= 1 expected_calls -= 1
return self.swiftclient return self.swiftclient

View File

@ -20,7 +20,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import cloudfiles from mox import IsA
from horizon import api from horizon import api
from horizon import exceptions from horizon import exceptions
@ -30,30 +30,32 @@ from horizon import test
class SwiftApiTests(test.APITestCase): class SwiftApiTests(test.APITestCase):
def test_swift_get_containers(self): def test_swift_get_containers(self):
containers = self.containers.list() containers = self.containers.list()
cont_data = [c._apidict for c in containers]
swift_api = self.stub_swiftclient() swift_api = self.stub_swiftclient()
swift_api.get_all_containers(limit=1001, swift_api.get_account(limit=1001,
marker=None).AndReturn(containers) marker=None,
full_listing=True).AndReturn([{}, cont_data])
self.mox.ReplayAll() self.mox.ReplayAll()
(conts, more) = api.swift_get_containers(self.request) (conts, more) = api.swift_get_containers(self.request)
self.assertEqual(len(conts), len(containers)) self.assertEqual(len(conts), len(containers))
self.assertFalse(more) self.assertFalse(more)
def test_swift_create_container(self): def test_swift_create_duplicate_container(self):
container = self.containers.first() container = self.containers.first()
swift_api = self.stub_swiftclient(expected_calls=2) swift_api = self.stub_swiftclient(expected_calls=2)
# Check for existence, then create # Check for existence, then create
exc = cloudfiles.errors.NoSuchContainer() exc = self.exceptions.swift
swift_api.get_container(container.name).AndRaise(exc) swift_api.head_container(container.name).AndRaise(exc)
swift_api.create_container(container.name).AndReturn(container) swift_api.put_container(container.name).AndReturn(container)
self.mox.ReplayAll() self.mox.ReplayAll()
# Verification handled by mox, no assertions needed. # Verification handled by mox, no assertions needed.
api.swift_create_container(self.request, container.name) 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() container = self.containers.first()
swift_api = self.stub_swiftclient() swift_api = self.stub_swiftclient()
swift_api.get_container(container.name).AndReturn(container) swift_api.head_container(container.name).AndReturn(container)
self.mox.ReplayAll() self.mox.ReplayAll()
# Verification handled by mox, no assertions needed. # Verification handled by mox, no assertions needed.
with self.assertRaises(exceptions.AlreadyExists): with self.assertRaises(exceptions.AlreadyExists):
@ -64,145 +66,55 @@ class SwiftApiTests(test.APITestCase):
objects = self.objects.list() objects = self.objects.list()
swift_api = self.stub_swiftclient() swift_api = self.stub_swiftclient()
swift_api.get_container(container.name).AndReturn(container) swift_api.get_container(container.name,
self.mox.StubOutWithMock(container, 'get_objects') limit=1001,
container.get_objects(limit=1001,
marker=None, marker=None,
prefix=None, prefix=None,
delimiter='/', delimiter='/',
path=None).AndReturn(objects) full_listing=True).AndReturn([{}, objects])
self.mox.ReplayAll() self.mox.ReplayAll()
(objs, more) = api.swift_get_objects(self.request, container.name) (objs, more) = api.swift_get_objects(self.request, container.name)
self.assertEqual(len(objs), len(objects)) self.assertEqual(len(objs), len(objects))
self.assertFalse(more) 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): def test_swift_upload_object(self):
container = self.containers.first() container = self.containers.first()
obj = self.objects.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 = self.stub_swiftclient()
swift_api.get_container(container.name).AndReturn(container) swift_api.put_object(container.name,
self.mox.StubOutWithMock(container, 'create_object') obj.name,
container.create_object(obj.name).AndReturn(obj) IsA(FakeFile),
self.mox.StubOutWithMock(obj, 'send') headers=headers)
obj.send(OBJECT_DATA).AndReturn(obj)
self.mox.ReplayAll() self.mox.ReplayAll()
ret_val = api.swift_upload_object(self.request, api.swift_upload_object(self.request,
container.name, container.name,
obj.name, obj.name,
OBJECT_DATA) FakeFile())
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)
def test_swift_object_exists(self): def test_swift_object_exists(self):
container = self.containers.first() container = self.containers.first()
obj = self.objects.first() obj = self.objects.first()
swift_api = self.stub_swiftclient(expected_calls=2) swift_api = self.stub_swiftclient(expected_calls=2)
self.mox.StubOutWithMock(container, 'get_object') swift_api.head_object(container.name, obj.name).AndReturn(container)
swift_api.get_container(container.name).AndReturn(container)
container.get_object(obj.name).AndReturn(obj) exc = self.exceptions.swift
swift_api.get_container(container.name).AndReturn(container) swift_api.head_object(container.name, obj.name).AndRaise(exc)
exc = cloudfiles.errors.NoSuchObject()
container.get_object(obj.name).AndRaise(exc)
self.mox.ReplayAll() self.mox.ReplayAll()
args = self.request, container.name, obj.name args = self.request, container.name, obj.name
self.assertTrue(api.swift_object_exists(*args)) self.assertTrue(api.swift_object_exists(*args))
# Again, for a "non-existent" object # Again, for a "non-existent" object
self.assertFalse(api.swift_object_exists(*args)) 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 keystoneclient import exceptions as keystone_exceptions
from novaclient import exceptions as nova_exceptions from novaclient import exceptions as nova_exceptions
from quantumclient.common import exceptions as quantum_exceptions from quantumclient.common import exceptions as quantum_exceptions
from swiftclient import client as swift_exceptions
from .utils import TestDataContainer from .utils import TestDataContainer
@ -57,3 +58,6 @@ def data(TEST):
quantum_exception = quantum_exceptions.QuantumClientException quantum_exception = quantum_exceptions.QuantumClientException
TEST.exceptions.quantum = create_stubbed_exception(quantum_exception) 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
import new from horizon.api import swift
from django import http
from cloudfiles import container, storage_object
from horizon.api import base
from .utils import TestDataContainer from .utils import TestDataContainer
@ -26,20 +20,8 @@ def data(TEST):
TEST.containers = TestDataContainer() TEST.containers = TestDataContainer()
TEST.objects = TestDataContainer() TEST.objects = TestDataContainer()
request = http.HttpRequest() container_1 = swift.Container(dict(name=u"container_one\u6346"))
request.user = TEST.user container_2 = swift.Container(dict(name=u"container_two\u6346"))
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")
TEST.containers.add(container_1, container_2) TEST.containers.add(container_1, container_2)
object_dict = {"name": u"test_object\u6346", object_dict = {"name": u"test_object\u6346",
@ -48,15 +30,10 @@ def data(TEST):
"last_modified": None, "last_modified": None,
"hash": u"object_hash"} "hash": u"object_hash"}
obj_dicts = [object_dict] obj_dicts = [object_dict]
obj_data = "Fake Data"
for obj_dict in obj_dicts: for obj_dict in obj_dicts:
swift_object = storage_object.Object(container_1, swift_object = swift.StorageObject(obj_dict,
object_record=obj_dict) container_1.name,
data=obj_data)
TEST.objects.add(swift_object) 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
from cloudfiles import errors as swiftclient
from glanceclient.common import exceptions as glanceclient from glanceclient.common import exceptions as glanceclient
from keystoneclient import exceptions as keystoneclient from keystoneclient import exceptions as keystoneclient
from novaclient import exceptions as novaclient from novaclient import exceptions as novaclient
from quantumclient.common import exceptions as quantumclient from quantumclient.common import exceptions as quantumclient
from swiftclient import client as swiftclient
UNAUTHORIZED = (keystoneclient.Unauthorized, UNAUTHORIZED = (keystoneclient.Unauthorized,
@ -31,17 +31,13 @@ UNAUTHORIZED = (keystoneclient.Unauthorized,
novaclient.Forbidden, novaclient.Forbidden,
glanceclient.Unauthorized, glanceclient.Unauthorized,
quantumclient.Unauthorized, quantumclient.Unauthorized,
quantumclient.Forbidden, quantumclient.Forbidden)
swiftclient.AuthenticationFailed,
swiftclient.AuthenticationError)
NOT_FOUND = (keystoneclient.NotFound, NOT_FOUND = (keystoneclient.NotFound,
novaclient.NotFound, novaclient.NotFound,
glanceclient.NotFound, glanceclient.NotFound,
quantumclient.NetworkNotFoundClient, quantumclient.NetworkNotFoundClient,
quantumclient.PortNotFoundClient, quantumclient.PortNotFoundClient)
swiftclient.NoSuchContainer,
swiftclient.NoSuchObject)
# NOTE(gabriel): This is very broad, and may need to be dialed in. # NOTE(gabriel): This is very broad, and may need to be dialed in.
RECOVERABLE = (keystoneclient.ClientException, RECOVERABLE = (keystoneclient.ClientException,
@ -57,4 +53,4 @@ RECOVERABLE = (keystoneclient.ClientException,
quantumclient.PortInUseClient, quantumclient.PortInUseClient,
quantumclient.AlreadyAttachedClient, quantumclient.AlreadyAttachedClient,
quantumclient.StateInvalidClient, quantumclient.StateInvalidClient,
swiftclient.Error) swiftclient.ClientException)

View File

@ -105,23 +105,3 @@
// Fluid grid // Fluid grid
@fluidGridColumnWidth: 6.382978723%; @fluidGridColumnWidth: 6.382978723%;
@fluidGridGutterWidth: 2.127659574%; @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;

View File

@ -528,7 +528,6 @@ table form {
.table_actions { .table_actions {
float: right; float: right;
min-width: 400px; min-width: 400px;
margin-bottom: 10px;
} }
.table_actions .table_search { .table_actions .table_search {
@ -1390,97 +1389,106 @@ label.log-length {
float: left; 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 { #browser_wrapper {
width: @browserWrapperWidth; width: @browserWrapperWidth;
> div{
position: relative;
padding: 55px 0 32px 0;
float: left;
background-color: @grayLighter; 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 { }
height: @tbodyHeight; }
border-left: @dataTableBorderWidth solid @dataTableBorderColor; form, table{
border-right: @dataTableBorderWidth solid @dataTableBorderColor; margin-bottom: 0;
overflow-y: scroll; }
overflow-x: hidden; .navigation_wrapper, .content_wrapper{
position: relative;
float: left;
} }
div.navigation_wrapper { div.navigation_wrapper {
width: @navigationTableWidth; width: @navigationTableWidth;
div.table_wrapper, div.table_wrapper,
thead th.table_header { thead th.table_header {
width: @navigationTableWidth - 2px; border-right: 0 none;
} border-top-right-radius: 0;
td {
background-color: whiteSmoke;
} }
td.normal_column{ td.normal_column{
width: @navigationColWidth; &:first-child {
min-width: @navigationColWidth; border-left: 0 none;
> a {
width: @navigationColWidth;
min-width: @navigationColWidth;
} }
} }
tfoot td { tfoot td {
width: @navigationTableWidth - 2 * @dataTableBorderWidth - 2 * @tableCellPadding; border-right: 0 none;
border-bottom-right-radius: 0;
} }
} }
div.content_wrapper { div.content_wrapper {
width: @contentTableWidth; width: @contentTableWidth;
div.table_wrapper, div.table_wrapper,
thead th.table_header { thead th.table_header {
width: @contentTableWidth - 2px; border-left: 0 none;
border-top-left-radius: 0;
} }
td.normal_column { td{
width: @contentColWidth; &:last-child {
min-width: @contentColWidth; border-right: 0 none;
> a {
width: @contentColWidth;
min-width: @contentColWidth;
} }
} }
tfoot td { tfoot td {
width: @contentTableWidth - 2 * @dataTableBorderWidth - 2 * @tableCellPadding; border-left: 0 none;
border-bottom-left-radius: 0;
} }
} }
table { table {
thead { thead {
position: absolute;
top: 0;
left: 0;
tr th { tr th {
border: @dataTableBorderWidth solid @dataTableBorderColor;
border-bottom: none; border-bottom: none;
background-color: @grayLighter; background-color: @grayLighter;
} }
} }
td.multi_select_column,
th.multi_select_column{
width: @multiSelectionWidth;
}
td.actions_column,
th.actions_column{
padding :@actionsColumnPadding;
width: @actionsColumnWidth;
}
tbody { tbody {
tr td:first-child{
border-left: none;
}
tr td:last-child {
border-right: none;
}
tr:last-child td { tr:last-child td {
border-bottom: @dataTableBorderWidth solid @dataTableBorderColor; 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;
} }
} }
tfoot td{
position: absolute;
left: 0;
bottom: 0;
} }
} }
} }

View File

@ -6,7 +6,7 @@ set -o errexit
# Increment me any time the environment should be rebuilt. # Increment me any time the environment should be rebuilt.
# This includes dependncy changes, directory renames, etc. # This includes dependncy changes, directory renames, etc.
# Simple integer secuence: 1, 2, 3... # Simple integer secuence: 1, 2, 3...
environment_version=26 environment_version=27
#--------------------------------------------------------# #--------------------------------------------------------#
function usage { function usage {

View File

@ -2,11 +2,11 @@
Django>=1.4 Django>=1.4
django_compressor django_compressor
django_openstack_auth django_openstack_auth
python-cloudfiles
python-glanceclient<2 python-glanceclient<2
python-keystoneclient python-keystoneclient
python-novaclient python-novaclient
python-quantumclient python-quantumclient
python-swiftclient>1.1,<1.2
pytz pytz
# Horizon Utility Requirements # Horizon Utility Requirements