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:
parent
ee17b1588b
commit
801c2321bf
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user