Merge "Removing deprecated Swift UI code"

This commit is contained in:
Jenkins 2016-11-24 07:55:16 +00:00 committed by Gerrit Code Review
commit a340ca2402
29 changed files with 33 additions and 1936 deletions

View File

@ -1,31 +0,0 @@
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from horizon import browsers
from openstack_dashboard.dashboards.project.containers import tables
class ContainerBrowser(browsers.ResourceBrowser):
name = "swift"
verbose_name = _("Swift")
navigation_table_class = tables.ContainersTable
content_table_class = tables.ObjectsTable
navigable_item_name = _("Container")
navigation_kwarg_name = "container_name"
content_kwarg_name = "subfolder_path"
has_breadcrumb = True
breadcrumb_url = "horizon:project:containers:index"

View File

@ -1,244 +0,0 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django.core import validators
from django.utils.encoding import force_text
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.containers import utils
no_slash_validator = validators.RegexValidator(r'^(?u)[^/]+$',
_("Slash is not an allowed "
"character."),
code="noslash")
no_begin_or_end_slash = validators.RegexValidator(r'^[^\/](?u).+[^\/]$',
_("Slash is not allowed at "
"the beginning or end of "
"your string."),
code="nobeginorendslash")
class CreateContainer(forms.SelfHandlingForm):
ACCESS_CHOICES = (
("private", _("Private")),
("public", _("Public")),
)
parent = forms.CharField(max_length=255,
required=False,
widget=forms.HiddenInput)
name = forms.CharField(max_length=255,
label=_("Container Name"),
validators=[no_slash_validator])
access = forms.ThemableChoiceField(label=_("Container Access"),
choices=ACCESS_CHOICES)
def handle(self, request, data):
try:
if not data['parent']:
is_public = data["access"] == "public"
metadata = ({'is_public': is_public})
# Create a container
api.swift.swift_create_container(request,
data["name"],
metadata=metadata)
messages.success(request, _("Container created successfully."))
else:
# Create a pseudo-folder
container, slash, remainder = data['parent'].partition("/")
remainder = remainder.rstrip("/")
subfolder_name = "/".join([bit for bit
in (remainder, data['name'])
if bit])
api.swift.swift_create_subfolder(request,
container,
subfolder_name)
messages.success(request, _("Folder created successfully."))
return True
except Exception:
exceptions.handle(request, _('Unable to create container.'))
class UploadObject(forms.SelfHandlingForm):
path = forms.CharField(max_length=255,
required=False,
widget=forms.HiddenInput)
object_file = forms.FileField(label=_("File"),
required=False,
allow_empty_file=True)
name = forms.CharField(max_length=255,
label=_("Object Name"),
help_text=_("Slashes are allowed, and are treated "
"as pseudo-folders by the Object "
"Store."),
widget=forms.TextInput(
attrs={"ng-model": "name",
"not-blank": ""}
))
container_name = forms.CharField(widget=forms.HiddenInput())
def _set_object_path(self, data):
if data['path']:
object_path = "/".join([data['path'].rstrip("/"), data['name']])
else:
object_path = data['name']
return object_path
def clean(self):
data = super(UploadObject, self).clean()
if 'object_file' not in self.files:
self.files['object_file'] = None
return data
def handle(self, request, data):
object_file = self.files['object_file']
object_path = self._set_object_path(data)
try:
obj = api.swift.swift_upload_object(request,
data['container_name'],
object_path,
object_file)
msg = force_text(_("Object was successfully uploaded."))
messages.success(request, msg)
return obj
except Exception:
exceptions.handle(request, _("Unable to upload object."))
class UpdateObject(UploadObject):
def __init__(self, *args, **kwargs):
super(UpdateObject, self).__init__(*args, **kwargs)
self.fields['name'].widget = forms.TextInput(
attrs={"readonly": "readonly"})
self.fields['name'].help_text = None
def handle(self, request, data):
object_file = self.files.get('object_file')
if object_file:
object_path = self._set_object_path(data)
try:
obj = api.swift.swift_upload_object(request,
data['container_name'],
object_path,
object_file)
messages.success(
request, _("Object was successfully updated."))
return obj
except Exception:
exceptions.handle(request, _("Unable to update object."))
return False
else:
# If object file is not provided, then a POST method is needed
# to update ONLY metadata. This must be implemented when
# object metadata can be updated from this panel.
return True
class CreatePseudoFolder(forms.SelfHandlingForm):
path = forms.CharField(max_length=255,
required=False,
widget=forms.HiddenInput)
name = forms.CharField(max_length=255,
label=_("Pseudo-folder Name"),
validators=[no_begin_or_end_slash])
container_name = forms.CharField(widget=forms.HiddenInput())
def _set_pseudo_folder_path(self, data):
if data['path']:
pseudo_folder_path = "/".join([data['path'].rstrip("/"),
data['name']]) + "/"
else:
pseudo_folder_path = data['name'] + "/"
return pseudo_folder_path
def handle(self, request, data):
pseudo_folder_path = self._set_pseudo_folder_path(data)
try:
obj = api.swift.swift_create_pseudo_folder(request,
data['container_name'],
pseudo_folder_path)
messages.success(request,
_("Pseudo-folder was successfully created."))
return obj
except Exception:
exceptions.handle(request, _("Unable to create pseudo-folder."))
class CopyObject(forms.SelfHandlingForm):
new_container_name = forms.ChoiceField(label=_("Destination container"),
validators=[no_slash_validator])
path = forms.CharField(
label=pgettext_lazy("Swift pseudo folder path", u"Path"),
max_length=255, required=False)
new_object_name = forms.CharField(max_length=255,
label=_("Destination object name"),
validators=[no_slash_validator])
orig_container_name = forms.CharField(widget=forms.HiddenInput())
orig_object_name = forms.CharField(widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
containers = kwargs.pop('containers')
super(CopyObject, self).__init__(*args, **kwargs)
self.fields['new_container_name'].choices = containers
def handle(self, request, data):
index = "horizon:project:containers:index"
orig_container = data['orig_container_name']
orig_object = data['orig_object_name']
new_container = data['new_container_name']
new_object = data['new_object_name']
path = data['path']
if path and not path.endswith("/"):
path = path + "/"
new_path = "%s%s" % (path, new_object)
# Now copy the object itself.
try:
api.swift.swift_copy_object(request,
orig_container,
orig_object,
new_container,
new_path)
dest = "%s/%s" % (new_container, path)
vals = {"dest": dest.rstrip("/"),
"orig": orig_object.split("/")[-1],
"new": new_object}
messages.success(request,
_('Copied "%(orig)s" to "%(dest)s" as "%(new)s".')
% vals)
return True
except exceptions.HorizonException as exc:
messages.error(request, exc)
raise exceptions.Http302(
reverse(index, args=[utils.wrap_delimiter(orig_container)]))
except Exception:
redirect = reverse(index,
args=[utils.wrap_delimiter(orig_container)])
exceptions.handle(request,
_("Unable to copy object."),
redirect=redirect)

View File

@ -1,432 +0,0 @@
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.core.urlresolvers import reverse
from django import shortcuts
from django import template
from django.template import defaultfilters as filters
from django.utils import http
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import messages
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.api import swift
from openstack_dashboard.dashboards.project.containers import utils
LOG = logging.getLogger(__name__)
class ViewContainer(tables.LinkAction):
name = "view"
verbose_name = _("View Details")
url = "horizon:project:containers:container_detail"
classes = ("ajax-modal", "btn-view")
def get_link_url(self, datum=None):
obj_id = self.table.get_object_id(datum)
return reverse(self.url, args=(obj_id,))
class MakePublicContainer(tables.Action):
name = "make_public"
verbose_name = _("Make Public")
icon = "pencil"
def allowed(self, request, container):
# Container metadata have not been loaded
if not hasattr(container, 'is_public'):
return False
return not container.is_public
def single(self, table, request, obj_id):
try:
api.swift.swift_update_container(request,
obj_id,
metadata=({'is_public': True}))
LOG.info('Updating container "%s" access to public.' % obj_id)
messages.success(request,
_('Successfully updated container access to '
'public.'))
except Exception:
exceptions.handle(request,
_('Unable to update container access.'))
return shortcuts.redirect('horizon:project:containers:index')
class MakePrivateContainer(tables.Action):
name = "make_private"
verbose_name = _("Make Private")
icon = "pencil"
def allowed(self, request, container):
# Container metadata have not been loaded
if not hasattr(container, 'is_public'):
return False
return container.is_public
def single(self, table, request, obj_id):
try:
api.swift.swift_update_container(request,
obj_id,
metadata=({'is_public': False}))
LOG.info('Updating container "%s" access to private.' % obj_id)
messages.success(request,
_('Successfully updated container access to '
'private.'))
except Exception:
exceptions.handle(request,
_('Unable to update container access.'))
return shortcuts.redirect('horizon:project:containers:index')
class DeleteContainer(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Container",
u"Delete Containers",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Container",
u"Deleted Containers",
count
)
success_url = "horizon:project:containers:index"
def delete(self, request, obj_id):
try:
api.swift.swift_delete_container(request, obj_id)
except exceptions.Conflict as exc:
exceptions.handle(request, exc, redirect=self.success_url)
except Exception:
exceptions.handle(request,
_('Unable to delete container.'),
redirect=self.success_url)
def get_success_url(self, request=None):
"""Returns the URL to redirect to after a successful action.
"""
current_container = self.table.kwargs.get("container_name", None)
# If the current_container is deleted, then redirect to the default
# completion url
if current_container in self.success_ids:
return self.success_url
return request.get_full_path()
class CreateContainer(tables.LinkAction):
name = "create"
verbose_name = _("Create Container")
url = "horizon:project:containers:create"
classes = ("ajax-modal",)
icon = "plus"
class ListObjects(tables.LinkAction):
name = "list_objects"
verbose_name = _("View Container")
url = "horizon:project:containers:index"
icon = "list"
def get_link_url(self, datum=None):
container_name = http.urlquote(datum.name)
args = (utils.wrap_delimiter(container_name),)
return reverse(self.url, args=args)
class CreatePseudoFolder(tables.LinkAction):
name = "create_pseudo_folder"
verbose_name = _("Create Pseudo-folder")
url = "horizon:project:containers:create_pseudo_folder"
classes = ("ajax-modal",)
icon = "plus"
def get_link_url(self, datum=None):
# Usable for both the container and object tables
if getattr(datum, 'container', datum):
container_name = http.urlquote(datum.name)
else:
container_name = self.table.kwargs['container_name']
subfolders = self.table.kwargs.get('subfolder_path', '')
args = (bit for bit in (container_name, subfolders) if bit)
return reverse(self.url, args=args)
def allowed(self, request, datum=None):
if self.table.kwargs.get('container_name', None):
return True
return False
def update(self, request, obj):
# This will only be called for the row, so we can remove the button
# styles meant for the table action version.
self.attrs = {'class': 'ajax-modal'}
class UploadObject(tables.LinkAction):
name = "upload"
verbose_name = _("Upload Object")
url = "horizon:project:containers:object_upload"
classes = ("ajax-modal",)
icon = "upload"
def get_link_url(self, datum=None):
# Usable for both the container and object tables
if getattr(datum, 'container', datum):
# This is a container
container_name = datum.name
else:
# This is a table action, and we already have the container name
container_name = self.table.kwargs['container_name']
subfolders = self.table.kwargs.get('subfolder_path', '')
args = (bit for bit in (container_name, subfolders) if bit)
return reverse(self.url, args=args)
def allowed(self, request, datum=None):
if self.table.kwargs.get('container_name', None):
return True
return False
def update(self, request, obj):
# This will only be called for the row, so we can remove the button
# styles meant for the table action version.
self.attrs = {'class': 'ajax-modal'}
def get_size_used(container):
return filters.filesizeformat(container.bytes)
def get_container_link(container):
return reverse("horizon:project:containers:index",
args=(utils.wrap_delimiter(container.name),))
class ContainerAjaxUpdateRow(tables.Row):
ajax = True
def get_data(self, request, container_name):
container = api.swift.swift_get_container(request,
container_name,
with_data=False)
return container
def get_metadata(container):
# If the metadata has not been loading, display a loading image
if not hasattr(container, 'is_public'):
return template.loader.render_to_string(
'project/containers/_container_loader.html'
)
template_name = 'project/containers/_container_metadata.html'
context = {"container": container}
return template.loader.render_to_string(template_name, context)
def get_metadata_loaded(container):
# Determine if metadata has been loaded if the attribute is already set.
return hasattr(container, 'is_public') and container.is_public is not None
class ContainersTable(tables.DataTable):
METADATA_LOADED_CHOICES = (
(False, None),
(True, True),
)
name = tables.Column("name",
link=get_container_link,
verbose_name=_("Container Name"))
metadata = tables.Column(get_metadata,
verbose_name=_("Container Details"),
classes=('nowrap-col', ),)
metadata_loaded = tables.Column(get_metadata_loaded,
status=True,
status_choices=METADATA_LOADED_CHOICES,
hidden=True)
class Meta(object):
name = "containers"
verbose_name = _("Containers")
row_class = ContainerAjaxUpdateRow
status_columns = ['metadata_loaded', ]
table_actions = (CreateContainer,)
row_actions = (ViewContainer, MakePublicContainer,
MakePrivateContainer, DeleteContainer,)
browser_table = "navigation"
footer = False
def get_object_id(self, container):
return container.name
class ViewObject(tables.LinkAction):
name = "view"
verbose_name = _("View Details")
url = "horizon:project:containers:object_detail"
classes = ("ajax-modal", "btn-view")
allowed_data_types = ("objects",)
def get_link_url(self, obj):
container_name = self.table.kwargs['container_name']
return reverse(self.url, args=(container_name, obj.name))
class UpdateObject(tables.LinkAction):
name = "update_object"
verbose_name = _("Edit")
url = "horizon:project:containers:object_update"
classes = ("ajax-modal",)
icon = "pencil"
allowed_data_types = ("objects",)
def get_link_url(self, obj):
container_name = self.table.kwargs['container_name']
return reverse(self.url, args=(container_name, obj.name))
class DeleteObject(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Object",
u"Delete Objects",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Object",
u"Deleted Objects",
count
)
name = "delete_object"
allowed_data_types = ("objects", "subfolders",)
def delete(self, request, obj_id):
obj = self.table.get_object_by_id(obj_id)
container_name = obj.container_name
datum_type = getattr(obj, self.table._meta.data_type_name, None)
if datum_type == 'subfolders':
obj_id = obj_id[(len(container_name) + 1):] + "/"
api.swift.swift_delete_object(request, container_name, obj_id)
class DeleteMultipleObjects(DeleteObject):
name = "delete_multiple_objects"
class CopyObject(tables.LinkAction):
name = "copy"
verbose_name = _("Copy")
url = "horizon:project:containers:object_copy"
classes = ("ajax-modal",)
icon = "circle-arrow-right"
allowed_data_types = ("objects",)
def get_link_url(self, obj):
container_name = self.table.kwargs['container_name']
return reverse(self.url, args=(container_name, obj.name))
class DownloadObject(tables.LinkAction):
name = "download"
verbose_name = _("Download")
url = "horizon:project:containers:object_download"
icon = "download"
allowed_data_types = ("objects",)
def get_link_url(self, obj):
container_name = self.table.kwargs['container_name']
return reverse(self.url, args=(container_name, obj.name))
def allowed(self, request, object):
return object.bytes and object.bytes > 0
class ObjectFilterAction(tables.FilterAction):
def _filtered_data(self, table, filter_string):
request = table.request
container = self.table.kwargs['container_name']
subfolder = self.table.kwargs['subfolder_path']
prefix = utils.wrap_delimiter(subfolder) if subfolder else ''
self.filtered_data = api.swift.swift_filter_objects(request,
filter_string,
container,
prefix=prefix)
return self.filtered_data
def filter_subfolders_data(self, table, objects, filter_string):
data = self._filtered_data(table, filter_string)
return [datum for datum in data if
datum.content_type == "application/pseudo-folder"]
def filter_objects_data(self, table, objects, filter_string):
data = self._filtered_data(table, filter_string)
return [datum for datum in data if
datum.content_type != "application/pseudo-folder"]
def allowed(self, request, datum=None):
if self.table.kwargs.get('container_name', None):
return True
return False
def sanitize_name(name):
return name.split(swift.FOLDER_DELIMITER)[-1]
def get_size(obj):
if obj.bytes is None:
return _("pseudo-folder")
return filters.filesizeformat(obj.bytes)
def get_link_subfolder(subfolder):
container_name = subfolder.container_name
return reverse("horizon:project:containers:index",
args=(utils.wrap_delimiter(container_name),
utils.wrap_delimiter(subfolder.name)))
class ObjectsTable(tables.DataTable):
name = tables.Column("name",
link=get_link_subfolder,
allowed_data_types=("subfolders",),
verbose_name=_("Object Name"),
filters=(sanitize_name,))
size = tables.Column(get_size, verbose_name=_('Size'))
class Meta(object):
name = "objects"
verbose_name = _("Objects")
table_actions = (ObjectFilterAction, CreatePseudoFolder, UploadObject,
DeleteMultipleObjects)
row_actions = (DownloadObject, UpdateObject, CopyObject,
ViewObject, DeleteObject)
data_types = ("subfolders", "objects")
browser_table = "content"
footer = False

View File

@ -1,29 +0,0 @@
{% extends "horizon/common/_modal.html" %}
{% load i18n %}
{% block modal-header %}{% trans "Container Details" %}{% endblock %}
{% block modal-body %}
<div class="info detail">
<dl class="dl-horizontal">
<dt>{% trans "Container Name" %}</dt>
<dd>{{ container.name }}</dd>
<dt>{% trans "Container Access" %}</dt>
{% if container.public_url %}
<dd>{% trans "Public" %}</dd>
<dt>{% trans "Public URL" %}</dt>
<dd>{{ container.public_url }}</dd>
{% else %}
<dd>{% trans "Private" %}</dd>
{% endif %}
<dt>{% trans "Object Count" %}</dt>
<dd>{{ container.container_object_count }}</dd>
<dt>{% trans "Size" %}</dt>
<dd>{{ container.container_bytes_used|filesizeformat }}</dd>
</dl>
</div>
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:project:containers:index' %}" class="btn btn-default cancel">{% trans "Close" %}</a>
{% endblock %}

View File

@ -1,5 +0,0 @@
<div class="horizon-loading-bar container-pending-bar">
<div class="progress progress-striped active">
<div class="progress-bar"></div>
</div>
</div>

View File

@ -1,12 +0,0 @@
{% load i18n %}
<ul>
<li>{% trans "Object Count: " %}{{ container.container_object_count }}</li>
<li>{% trans "Size: " %}{{ container.container_bytes_used|filesizeformat }}</li>
<li>{% trans "Access: " %}
{% if container.public_url %}
<a href="{{ container.public_url }}">{% trans "Public" %}</a>
{% else %}
{% trans "Private" %}
{% endif %}
</li>
</ul>

View File

@ -1,24 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}copy_object_form{% endblock %}
{% block form_action %}{% url 'horizon:project:containers:object_copy' container_name object_name %}{% endblock %}
{% block modal-header %}{% blocktrans %}Copy Object: {{ object_name }}{% endblocktrans %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Make a new copy of an existing object to store in this or another container. You may additionally specify the path within the selected container where the new copy should be stored." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Copy Object" %}" />
<a href="{% url 'horizon:project:containers:index' container_name|add:'/' %}" class="btn btn-default cancel">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}create_container_form{% endblock %}
{% block form_action %}{% url 'horizon:project:containers:create' %}{% endblock %}
{% block modal-header %}{% trans "Create Container" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>{% trans "A container is a storage compartment for your data and provides a way for you to organize your data. You can think of a container as a folder in Windows &reg; or a directory in UNIX &reg;. The primary difference between a container and these other file system concepts is that containers cannot be nested. You can, however, create an unlimited number of containers within your account. Data must be stored in a container so you must have at least one container defined in your account prior to uploading data." %}</p>
<p>{% trans "Note: A Public Container will allow anyone with the Public URL to gain access to your objects in the container." %}</p>
</div>
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}create_directory_form{% endblock %}
{% block form_action %}{% url 'horizon:project:containers:create_pseudo_folder' container_name %}{% endblock %}
{% block modal-header %}
{% blocktrans %}Create pseudo-folder in container {{ container_name }}{% endblocktrans %}
{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p><strong>{% trans "Pseudo-folder:" %}</strong> {% trans "Within a container you can group your objects into pseudo-folders, which behave similarly to folders in your desktop operating system, with the exception that they are virtual collections defined by a common prefix on the object's name. A slash (/) character is used as the delimiter for pseudo-folders in the Object Store." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:project:containers:index' container_name|add:'/' %}" class="btn btn-default cancel">{% trans "Cancel" %}</a>
<input class="btn btn-primary" type="submit" value="{% trans "Create" %}" />
{% endblock %}

View File

@ -1,25 +0,0 @@
{% extends "horizon/common/_modal.html" %}
{% load i18n %}
{% block modal-header %}{% trans "Object Details" %}{% endblock %}
{% block modal-body %}
<div class="info detail">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd>{{ object.name }}</dd>
<dt>{% trans "Hash" %}</dt>
<dd>{{ object.etag }}</dd>
<dt>{% trans "Content Type" %}</dt>
<dd>{{ object.content_type }}</dd>
<dt>{% trans "Last Modified" %}</dt>
<dd>{{ object.timestamp|parse_isotime }}</dd>
<dt>{% trans "Size" %}</dt>
<dd>{{ object.bytes|filesizeformat }}</dd>
</dl>
</div>
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:project:containers:index' %}" class="btn btn-default cancel">{% trans "Close" %}</a>
{% endblock %}

View File

@ -1,28 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}update_object_form{% endblock %}
{% block ng_controller %}DummyController{% endblock %}
{% block form_name %}updateForm{% endblock %}
{% block form_action %}{% url 'horizon:project:containers:object_update' container_name subfolder_path object_path %}{% endblock %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-header %}{% trans "Edit Object" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p><strong>{% trans "Object:" %}</strong> {% trans "An object is the basic storage entity that represents a file you store in the OpenStack Object Storage system. When you upload data to OpenStack Object Storage, the data is stored as-is (no compression or encryption) and consists of a location (container), the object's name, and any metadata consisting of key/value pairs." %}</p>
<p><strong>{% trans "File:" %}</strong> {% trans "A new uploaded file will replace the content of the current object" %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:project:containers:index' container_name|add:'/' %}" class="btn btn-default cancel">{% trans "Cancel" %}</a>
<input class="btn btn-primary" type="submit" ng-disabled="updateForm.$invalid" value="{% trans "Update Object" %}" />
{% endblock %}

View File

@ -1,29 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}upload_object_form{% endblock %}
{% block form_name %}uploadForm{% endblock %}
{% block form_action %}{% url 'horizon:project:containers:object_upload' container_name %}{% endblock %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-header %}
{% blocktrans %}Upload Object To Container: {{ container_name }}{% endblocktrans %}
{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p><strong>{% trans "Object:" %}</strong> {% trans "An object is the basic storage entity that represents a file you store in the OpenStack Object Storage system. When you upload data to OpenStack Object Storage, the data is stored as-is (no compression or encryption) and consists of a location (container), the object's name, and any metadata consisting of key/value pairs." %}</p>
<p><strong>{% trans "Pseudo-folder:" %}</strong> {% trans "Within a container you can group your objects into pseudo-folders, which behave similarly to folders in your desktop operating system, with the exception that they are virtual collections defined by a common prefix on the object's name. A slash (/) character is used as the delimiter for pseudo-folders in the Object Store." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:project:containers:index' container_name|add:'/' %}" class="btn btn-default cancel">{% trans "Cancel" %}</a>
<input class="btn btn-primary" type="submit" ng-disabled="uploadForm.$invalid || uploadForm.$pristine" value="{% trans "Upload Object" %}" />
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Container Details" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-sm-12">
{% include 'project/containers/_container_detail.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Copy Object" %}{% endblock %}
{% block main %}
{% include 'project/containers/_copy.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Container" %}{% endblock %}
{% block main %}
{% include "project/containers/_create.html" %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Pseudo-folder" %}{% endblock %}
{% block main %}
{% include 'project/containers/_create_pseudo_folder.html' %}
{% endblock %}

View File

@ -1,24 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Containers" %}{% endblock %}
{% block main %}
{% if subfolders %}
<div class="page_title table_header">
<div>
<h3>
<a href="{% url 'horizon:project:containers:index' %}{{ container_name }}/">{{ container_name }}</a> : /
{% for folder in subfolders %}
{% if forloop.last %}
{{ folder.0 }}
{% else %}
<a href="{% url 'horizon:project:containers:index' %}{{ container_name }}/{{ folder.1 }}">{{ folder.0 }}</a> /
{% endif %}
{% endfor %}
</h3>
</div>
</div>
{% endif %}
{{ swift_browser.render }}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Object Details" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-sm-12">
{% include 'project/containers/_object_detail.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Object" %}{% endblock %}
{% block main %}
{% include 'project/containers/_update.html' %}
{% endblock %}

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Upload Object" %}{% endblock %}
{% block main %}
{% include 'project/containers/_upload.html' %}
{% endblock %}

View File

@ -1,565 +0,0 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import email.header
import tempfile
import django
from django.core.files.uploadedfile import InMemoryUploadedFile # noqa
from django.core.urlresolvers import reverse
from django import http
from django.utils import http as utils_http
from mox3.mox import IsA # noqa
import six
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.containers import forms
from openstack_dashboard.dashboards.project.containers import tables
from openstack_dashboard.dashboards.project.containers import utils
from openstack_dashboard.dashboards.project.containers import views
from openstack_dashboard.test import helpers as test
CONTAINER_NAME_1 = u"container one%\u6346"
CONTAINER_NAME_2 = u"container_two\u6346"
CONTAINER_NAME_1_QUOTED = utils_http.urlquote(CONTAINER_NAME_1)
CONTAINER_NAME_2_QUOTED = utils_http.urlquote(CONTAINER_NAME_2)
INVALID_CONTAINER_NAME_1 = utils_http.urlquote(CONTAINER_NAME_1_QUOTED)
INVALID_CONTAINER_NAME_2 = utils_http.urlquote(CONTAINER_NAME_2_QUOTED)
CONTAINER_INDEX_URL = reverse('horizon:project:containers:index')
INVALID_PATHS = []
def invalid_paths():
if not INVALID_PATHS:
for x in (CONTAINER_NAME_1_QUOTED, CONTAINER_NAME_2_QUOTED):
y = reverse('horizon:project:containers:index',
args=(utils.wrap_delimiter(x), ))
INVALID_PATHS.append(y)
for x in (CONTAINER_NAME_1, CONTAINER_NAME_2):
INVALID_PATHS.append(CONTAINER_INDEX_URL + x)
return INVALID_PATHS
class SwiftTests(test.TestCase):
def _test_invalid_paths(self, response):
for x in invalid_paths():
self.assertNotContains(response, x)
@test.create_stubs({api.swift: ('swift_get_containers',)})
def test_index_no_container_selected(self):
containers = self.containers.list()
api.swift.swift_get_containers(IsA(http.HttpRequest), marker=None) \
.AndReturn((containers, False))
self.mox.ReplayAll()
res = self.client.get(CONTAINER_INDEX_URL)
self.assertTemplateUsed(res, 'project/containers/index.html')
self.assertIn('table', res.context)
resp_containers = res.context['table'].data
self.assertEqual(len(resp_containers), len(containers))
@test.create_stubs({api.swift: ('swift_delete_container', )})
def test_delete_container(self):
for container in self.containers.list():
self.mox.ResetAll() # mandatory in a for loop
api.swift.swift_delete_container(IsA(http.HttpRequest),
container.name)
self.mox.ReplayAll()
action_string = u"containers__delete__%s" % container.name
form_data = {"action": action_string}
req = self.factory.post(CONTAINER_INDEX_URL, form_data)
table = tables.ContainersTable(req, self.containers.list())
handled = table.maybe_handle()
self.assertEqual(handled['location'], CONTAINER_INDEX_URL)
@test.create_stubs({api.swift: ('swift_get_objects', )})
def test_delete_container_nonempty(self):
container = self.containers.first()
objects = self.objects.list()
api.swift.swift_get_objects(IsA(http.HttpRequest),
container.name).AndReturn([objects, False])
self.mox.ReplayAll()
action_string = u"containers__delete__%s" % container.name
form_data = {"action": action_string}
req = self.factory.post(CONTAINER_INDEX_URL, form_data)
req.META['HTTP_REFERER'] = '%s/%s' % (CONTAINER_INDEX_URL,
container.name)
table = tables.ContainersTable(req, self.containers.list())
handled = table.maybe_handle()
self.assertEqual(handled.status_code, 302)
self.assertEqual(six.text_type(list(req._messages)[0].message),
u"The container cannot be deleted "
u"since it is not empty.")
def test_create_container_get(self):
res = self.client.get(reverse('horizon:project:containers:create'))
self.assertTemplateUsed(res, 'project/containers/create.html')
@test.create_stubs({api.swift: ('swift_create_container',)})
def test_create_container_post(self):
for container in self.containers.list():
self.mox.ResetAll() # mandatory in a for loop
api.swift.swift_create_container(IsA(http.HttpRequest),
container.name,
metadata=({'is_public': False}))
self.mox.ReplayAll()
formData = {'name': container.name,
'access': "private",
'method': forms.CreateContainer.__name__}
res = self.client.post(
reverse('horizon:project:containers:create'), formData)
args = (utils.wrap_delimiter(container.name),)
url = reverse('horizon:project:containers:index', args=args)
self.assertRedirectsNoFollow(res, url)
@test.create_stubs({api.swift: ('swift_update_container', )})
def test_update_container_to_public(self):
container = self.containers.get(name=u"container one%\u6346")
api.swift.swift_update_container(IsA(http.HttpRequest),
container.name,
metadata=({'is_public': True}))
self.mox.ReplayAll()
action_string = u"containers__make_public__%s" % container.name
form_data = {"action": action_string}
req = self.factory.post(CONTAINER_INDEX_URL, form_data)
table = tables.ContainersTable(req, self.containers.list())
handled = table.maybe_handle()
self.assertEqual(handled['location'], CONTAINER_INDEX_URL)
@test.create_stubs({api.swift: ('swift_update_container', )})
def test_update_container_to_private(self):
container = self.containers.get(name=u"container_two\u6346")
api.swift.swift_update_container(IsA(http.HttpRequest),
container.name,
metadata=({'is_public': False}))
self.mox.ReplayAll()
action_string = u"containers__make_private__%s" % container.name
form_data = {"action": action_string}
req = self.factory.post(CONTAINER_INDEX_URL, form_data)
table = tables.ContainersTable(req, self.containers.list())
handled = table.maybe_handle()
self.assertEqual(handled['location'], CONTAINER_INDEX_URL)
@test.create_stubs({api.swift: ('swift_get_containers',
'swift_get_objects')})
def test_index_container_selected(self):
containers = (self.containers.list(), False)
ret = (self.objects.list(), False)
api.swift.swift_get_containers(IsA(http.HttpRequest),
marker=None).AndReturn(containers)
api.swift.swift_get_objects(IsA(http.HttpRequest),
self.containers.first().name,
marker=None,
prefix=None).AndReturn(ret)
self.mox.ReplayAll()
container_name = self.containers.first().name
res = self.client.get(
reverse('horizon:project:containers:index',
args=[utils.wrap_delimiter(container_name)]))
self.assertTemplateUsed(res, 'project/containers/index.html')
# UTF8 encoding here to ensure there aren't problems with Nose output.
expected = [obj.name.encode('utf8') for obj in self.objects.list()]
self.assertQuerysetEqual(res.context['objects_table'].data,
expected,
lambda obj: obj.name.encode('utf8'))
# Check if the two forms' URL are properly 'urlquote()d'.
form_action = ' action="%s%s/" ' % (CONTAINER_INDEX_URL,
CONTAINER_NAME_1_QUOTED)
self.assertContains(res, form_action, count=2)
self._test_invalid_paths(res)
@test.create_stubs({api.swift: ('swift_upload_object',)})
def test_upload(self):
container = self.containers.first()
obj = self.objects.first()
OBJECT_DATA = b'objectData'
temp_file = tempfile.NamedTemporaryFile()
temp_file.write(OBJECT_DATA)
temp_file.flush()
temp_file.seek(0)
api.swift.swift_upload_object(IsA(http.HttpRequest),
container.name,
obj.name,
IsA(InMemoryUploadedFile)).AndReturn(obj)
self.mox.ReplayAll()
upload_url = reverse('horizon:project:containers:object_upload',
args=[container.name])
res = self.client.get(upload_url)
self.assertTemplateUsed(res, 'project/containers/upload.html')
self.assertContains(res, 'enctype="multipart/form-data"')
self._test_invalid_paths(res)
formData = {'method': forms.UploadObject.__name__,
'container_name': container.name,
'name': obj.name,
'object_file': temp_file}
res = self.client.post(upload_url, formData)
args = (utils.wrap_delimiter(container.name),)
index_url = reverse('horizon:project:containers:index', args=args)
self.assertRedirectsNoFollow(res, index_url)
@test.create_stubs({api.swift: ('swift_upload_object',)})
def test_upload_without_file(self):
container = self.containers.first()
obj = self.objects.first()
api.swift.swift_upload_object(IsA(http.HttpRequest),
container.name,
obj.name,
None).AndReturn(obj)
self.mox.ReplayAll()
upload_url = reverse('horizon:project:containers:object_upload',
args=[container.name])
res = self.client.get(upload_url)
self.assertTemplateUsed(res, 'project/containers/upload.html')
res = self.client.get(upload_url)
self.assertContains(res, 'enctype="multipart/form-data"')
self.assertNotContains(res, INVALID_CONTAINER_NAME_1)
self.assertNotContains(res, INVALID_CONTAINER_NAME_2)
formData = {'method': forms.UploadObject.__name__,
'container_name': container.name,
'name': obj.name,
'object_file': None}
res = self.client.post(upload_url, formData)
args = (utils.wrap_delimiter(container.name),)
index_url = reverse('horizon:project:containers:index', args=args)
self.assertRedirectsNoFollow(res, index_url)
@test.create_stubs({api.swift: ('swift_create_pseudo_folder',)})
def test_create_pseudo_folder(self):
container = self.containers.first()
obj = self.objects.first()
api.swift.swift_create_pseudo_folder(IsA(http.HttpRequest),
container.name,
obj.name + "/").AndReturn(obj)
self.mox.ReplayAll()
create_pseudo_folder_url = reverse('horizon:project:containers:'
'create_pseudo_folder',
args=[container.name])
res = self.client.get(create_pseudo_folder_url)
self.assertTemplateUsed(res,
'project/containers/create_pseudo_folder.html')
self._test_invalid_paths(res)
formData = {'method': forms.CreatePseudoFolder.__name__,
'container_name': container.name,
'name': obj.name}
res = self.client.post(create_pseudo_folder_url, formData)
index_url = reverse('horizon:project:containers:index',
args=[utils.wrap_delimiter(container.name)])
self.assertRedirectsNoFollow(res, index_url)
@test.create_stubs({api.swift: ('swift_delete_object',)})
def test_delete(self):
container = self.containers.first()
obj = self.objects.first()
args = (utils.wrap_delimiter(container.name),)
index_url = reverse('horizon:project:containers:index', args=args)
api.swift.swift_delete_object(IsA(http.HttpRequest),
container.name,
obj.name)
self.mox.ReplayAll()
action_string = "objects__delete_object__%s" % obj.name
form_data = {"action": action_string}
req = self.factory.post(index_url, form_data)
kwargs = {"container_name": container.name}
table = tables.ObjectsTable(req, self.objects.list(), **kwargs)
handled = table.maybe_handle()
self.assertEqual(handled['location'], index_url)
@test.create_stubs({api.swift: ('swift_delete_object',)})
def test_delete_pseudo_folder(self):
container = self.containers.first()
folder = self.folder.first()
args = (utils.wrap_delimiter(container.name),)
index_url = reverse('horizon:project:containers:index', args=args)
api.swift.swift_delete_object(IsA(http.HttpRequest),
container.name,
folder.name + '/')
self.mox.ReplayAll()
action_string = "objects__delete_object__%s/%s" % (container.name,
folder.name)
form_data = {"action": action_string}
req = self.factory.post(index_url, form_data)
kwargs = {"container_name": container.name}
table = tables.ObjectsTable(req, self.folder.list(), **kwargs)
handled = table.maybe_handle()
self.assertEqual(handled['location'], index_url)
@test.create_stubs({api.swift: ('swift_get_object',)})
def test_download(self):
for container in self.containers.list():
for obj in self.objects.list():
self.mox.ResetAll() # mandatory in a for loop
obj = copy.copy(obj)
_data = obj.data
def make_iter():
yield _data
obj.data = make_iter()
api.swift.swift_get_object(
IsA(http.HttpRequest),
container.name,
obj.name,
resp_chunk_size=api.swift.CHUNK_SIZE).AndReturn(obj)
self.mox.ReplayAll()
download_url = reverse(
'horizon:project:containers:object_download',
args=[container.name, obj.name])
res = self.client.get(download_url)
self.assertTrue(res.has_header('Content-Disposition'))
self.assertEqual(b''.join(res.streaming_content), _data)
self.assertNotContains(res, INVALID_CONTAINER_NAME_1)
self.assertNotContains(res, INVALID_CONTAINER_NAME_2)
# Check that the returned Content-Disposition filename is
# correct - some have commas which must be removed
expected_name = obj.name.replace(',', '')
# some have a path which must be removed
if '/' in expected_name:
expected_name = expected_name.split('/')[-1]
# There will also be surrounding double quotes
expected_name = '"' + expected_name + '"'
expected = 'attachment; filename=%s' % expected_name
content = res.get('Content-Disposition')
if six.PY3:
header = email.header.decode_header(content)
content = header[0][0]
if isinstance(content, str):
content = content.encode('utf-8')
expected = expected.encode('utf-8')
self.assertEqual(content, expected)
@test.create_stubs({api.swift: ('swift_get_containers',)})
def test_copy_index(self):
ret = (self.containers.list(), False)
api.swift.swift_get_containers(IsA(http.HttpRequest)).AndReturn(ret)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:containers:object_copy',
args=[self.containers.first().name,
self.objects.first().name]))
self.assertTemplateUsed(res, 'project/containers/copy.html')
self.assertNotContains(res, INVALID_CONTAINER_NAME_1)
self.assertNotContains(res, INVALID_CONTAINER_NAME_2)
@test.create_stubs({api.swift: ('swift_get_containers',
'swift_copy_object')})
def test_copy(self):
container_1 = self.containers.get(name=CONTAINER_NAME_1)
container_2 = self.containers.get(name=CONTAINER_NAME_2)
obj = self.objects.first()
ret = (self.containers.list(), False)
api.swift.swift_get_containers(IsA(http.HttpRequest)).AndReturn(ret)
api.swift.swift_copy_object(IsA(http.HttpRequest),
container_1.name,
obj.name,
container_2.name,
obj.name)
self.mox.ReplayAll()
formData = {'method': forms.CopyObject.__name__,
'new_container_name': container_2.name,
'new_object_name': obj.name,
'orig_container_name': container_1.name,
'orig_object_name': obj.name}
copy_url = reverse('horizon:project:containers:object_copy',
args=[container_1.name, obj.name])
res = self.client.post(copy_url, formData)
args = (utils.wrap_delimiter(container_2.name),)
index_url = reverse('horizon:project:containers:index', args=args)
self.assertRedirectsNoFollow(res, index_url)
@test.create_stubs({api.swift: ('swift_get_containers',
'swift_copy_object')})
def test_copy_get(self):
original_name = u"test folder%\u6346/test.txt"
copy_name = u"test.copy.txt"
container = self.containers.first()
obj = self.objects.get(name=original_name)
ret = (self.containers.list(), False)
api.swift.swift_get_containers(IsA(http.HttpRequest)).AndReturn(ret)
self.mox.ReplayAll()
copy_url = reverse('horizon:project:containers:object_copy',
args=[container.name, obj.name])
res = self.client.get(copy_url)
# The copy's name must appear in initial data
if django.VERSION >= (1, 10):
pattern = ('<input id="id_new_object_name" value="%s" '
'name="new_object_name" type="text" '
'class="form-control" '
'maxlength="255" required/>' % copy_name)
else:
pattern = ('<input id="id_new_object_name" value="%s" '
'name="new_object_name" type="text" '
'class="form-control" '
'maxlength="255"/>' % copy_name)
self.assertContains(res, pattern, html=True)
def test_get_copy_name(self):
self.assertEqual(views.CopyView.get_copy_name('test.txt'),
'test.copy.txt')
self.assertEqual(views.CopyView.get_copy_name('test'),
'test.copy')
@test.create_stubs({api.swift: ('swift_upload_object',)})
def test_update_with_file(self):
container = self.containers.first()
obj = self.objects.first()
OBJECT_DATA = b'objectData'
temp_file = tempfile.NamedTemporaryFile()
temp_file.write(OBJECT_DATA)
temp_file.flush()
temp_file.seek(0)
api.swift.swift_upload_object(IsA(http.HttpRequest),
container.name,
obj.name,
IsA(InMemoryUploadedFile)).AndReturn(obj)
self.mox.ReplayAll()
update_url = reverse('horizon:project:containers:object_update',
args=[container.name, obj.name])
res = self.client.get(update_url)
self.assertTemplateUsed(res, 'project/containers/update.html')
self.assertContains(res, 'enctype="multipart/form-data"')
self._test_invalid_paths(res)
formData = {'method': forms.UpdateObject.__name__,
'container_name': container.name,
'name': obj.name,
'object_file': temp_file}
res = self.client.post(update_url, formData)
args = (utils.wrap_delimiter(container.name),)
index_url = reverse('horizon:project:containers:index', args=args)
self.assertRedirectsNoFollow(res, index_url)
@test.create_stubs({api.swift: ('swift_upload_object',)})
def test_update_without_file(self):
container = self.containers.first()
obj = self.objects.first()
self.mox.ReplayAll()
update_url = reverse('horizon:project:containers:object_update',
args=[container.name, obj.name])
res = self.client.get(update_url)
self.assertTemplateUsed(res, 'project/containers/update.html')
self.assertContains(res, 'enctype="multipart/form-data"')
self._test_invalid_paths(res)
formData = {'method': forms.UpdateObject.__name__,
'container_name': container.name,
'name': obj.name}
res = self.client.post(update_url, formData)
args = (utils.wrap_delimiter(container.name),)
index_url = reverse('horizon:project:containers:index', args=args)
self.assertRedirectsNoFollow(res, index_url)
@test.create_stubs({api.swift: ('swift_get_container', )})
def test_view_container(self):
for container in self.containers.list():
self.mox.ResetAll() # mandatory in a for loop
api.swift.swift_get_container(IsA(http.HttpRequest),
container.name,
with_data=False) \
.AndReturn(container)
self.mox.ReplayAll()
view_url = reverse('horizon:project:containers:container_detail',
args=[container.name])
res = self.client.get(view_url)
self.assertTemplateUsed(res,
'project/containers/container_detail.html')
self.assertContains(res, container.name, 1, 200)
self.assertNotContains(res, INVALID_CONTAINER_NAME_1)
self.assertNotContains(res, INVALID_CONTAINER_NAME_2)
@test.create_stubs({api.swift: ('swift_get_object', )})
def test_view_object(self):
for container in self.containers.list():
for obj in self.objects.list():
self.mox.ResetAll() # mandatory in a for loop
api.swift.swift_get_object(IsA(http.HttpRequest),
container.name,
obj.name,
with_data=False) \
.AndReturn(obj)
self.mox.ReplayAll()
view_url = reverse('horizon:project:containers:object_detail',
args=[container.name, obj.name])
res = self.client.get(view_url)
self.assertTemplateUsed(
res, 'project/containers/object_detail.html')
self.assertContains(res, obj.name, 1, 200)
self._test_invalid_paths(res)
def test_wrap_delimiter(self):
expected = {
'containerA': 'containerA/',
'containerB%': 'containerB%/', # no urlquote() should occur
'containerC/': 'containerC/', # already wrapped name
'containerD/objectA': 'containerD/objectA/'
}
for name, expected_name in expected.items():
self.assertEqual(utils.wrap_delimiter(name), expected_name)

View File

@ -16,60 +16,14 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from django.conf.urls import url
from openstack_dashboard.dashboards.project.containers import views
if settings.HORIZON_CONFIG['swift_panel'] == 'angular':
# New angular containers and objects
urlpatterns = [
url(r'^container/((?P<container_name>.+?)/)?'
'(?P<subfolder_path>.+)?$',
views.NgIndexView.as_view(), name='index'),
url(r'^$',
views.NgIndexView.as_view(), name='index')
]
else:
# Legacy swift containers and objects
urlpatterns = [
url(r'^((?P<container_name>.+?)/)?(?P<subfolder_path>(.+/)+)?$',
views.ContainerView.as_view(), name='index'),
url(r'^(?P<container_name>(.+/)+)?create$',
views.CreateView.as_view(),
name='create'),
url(r'^(?P<container_name>.+?)/(?P<subfolder_path>(.+/)+)'
'?container_detail$',
views.ContainerDetailView.as_view(),
name='container_detail'),
url(r'^(?P<container_name>[^/]+)/(?P<object_path>.+)/object_detail$',
views.ObjectDetailView.as_view(),
name='object_detail'),
url(r'^(?P<container_name>[^/]+)/(?P<subfolder_path>(.+/)+)?'
'(?P<object_name>.+)/update$',
views.UpdateObjectView.as_view(),
name='object_update'),
url(r'^(?P<container_name>.+?)/(?P<subfolder_path>(.+/)+)?upload$',
views.UploadView.as_view(),
name='object_upload'),
url(r'^(?P<container_name>.+?)/(?P<subfolder_path>(.+/)+)'
'?create_pseudo_folder',
views.CreatePseudoFolderView.as_view(),
name='create_pseudo_folder'),
url(r'^(?P<container_name>[^/]+)/'
r'(?P<subfolder_path>(.+/)+)?'
r'(?P<object_name>.+)/copy$',
views.CopyView.as_view(),
name='object_copy'),
url(r'^(?P<container_name>[^/]+)/(?P<object_path>.+)/download$',
views.object_download,
name='object_download'),
]
urlpatterns = [
url(r'^container/((?P<container_name>.+?)/)?'
'(?P<subfolder_path>.+)?$',
views.NgIndexView.as_view(), name='index'),
url(r'^$',
views.NgIndexView.as_view(), name='index')
]

View File

@ -10,9 +10,23 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.core import validators
from django.utils.translation import ugettext_lazy as _
from openstack_dashboard.api import swift
no_slash_validator = validators.RegexValidator(r'^(?u)[^/]+$',
_("Slash is not an allowed "
"character."),
code="noslash")
no_begin_or_end_slash = validators.RegexValidator(r'^[^\/](?u).+[^\/]$',
_("Slash is not allowed at "
"the beginning or end of "
"your string."),
code="nobeginorendslash")
def wrap_delimiter(name):
if name and not name.endswith(swift.FOLDER_DELIMITER):
return name + swift.FOLDER_DELIMITER

View File

@ -16,327 +16,10 @@
# License for the specific language governing permissions and limitations
# under the License.
"""
Views for managing Swift containers.
"""
import os
from django.core.urlresolvers import reverse
from django import http
from django.utils.functional import cached_property # noqa
from django.utils.translation import ugettext_lazy as _
from django.views import generic
import six
from horizon import browsers
from horizon import exceptions
from horizon import forms
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.api import swift
from openstack_dashboard.dashboards.project.containers \
import browsers as project_browsers
from openstack_dashboard.dashboards.project.containers \
import forms as project_forms
from openstack_dashboard.dashboards.project.containers import utils
class NgIndexView(generic.TemplateView):
"""View for managing Swift containers."""
template_name = 'project/containers/ngindex.html'
class ContainerView(browsers.ResourceBrowserView):
browser_class = project_browsers.ContainerBrowser
template_name = "project/containers/index.html"
page_title = _("Containers")
def get_containers_data(self):
containers = []
self._more = None
marker = self.request.GET.get('marker', None)
try:
containers, self._more = api.swift.swift_get_containers(
self.request, marker=marker)
except Exception:
msg = _('Unable to retrieve container list.')
exceptions.handle(self.request, msg)
return containers
@cached_property
def objects(self):
"""Returns a list of objects given the subfolder's path.
The path is from the kwargs of the request.
"""
objects = []
self._more = None
marker = self.request.GET.get('marker', None)
container_name = self.kwargs['container_name']
subfolder = self.kwargs['subfolder_path']
prefix = None
if container_name:
self.navigation_selection = True
if subfolder:
prefix = subfolder
try:
objects, self._more = api.swift.swift_get_objects(
self.request,
container_name,
marker=marker,
prefix=prefix)
except Exception:
self._more = None
objects = []
msg = _('Unable to retrieve object list.')
exceptions.handle(self.request, msg)
return objects
def is_subdir(self, item):
content_type = "application/pseudo-folder"
return getattr(item, "content_type", None) == content_type
def is_placeholder(self, item):
object_name = getattr(item, "name", "")
return object_name.endswith(api.swift.FOLDER_DELIMITER)
def get_objects_data(self):
"""Returns a list of objects within the current folder."""
filtered_objects = [item for item in self.objects
if (not self.is_subdir(item) and
not self.is_placeholder(item))]
return filtered_objects
def get_subfolders_data(self):
"""Returns a list of subfolders within the current folder."""
filtered_objects = [item for item in self.objects
if self.is_subdir(item)]
return filtered_objects
def get_context_data(self, **kwargs):
context = super(ContainerView, self).get_context_data(**kwargs)
context['container_name'] = self.kwargs["container_name"]
context['subfolders'] = []
if self.kwargs["subfolder_path"]:
(parent, slash, folder) = self.kwargs["subfolder_path"] \
.strip('/').rpartition('/')
while folder:
path = "%s%s%s/" % (parent, slash, folder)
context['subfolders'].insert(0, (folder, path))
(parent, slash, folder) = parent.rpartition('/')
return context
class CreateView(forms.ModalFormView):
form_class = project_forms.CreateContainer
template_name = 'project/containers/create.html'
success_url = "horizon:project:containers:index"
page_title = _("Create Container")
def get_success_url(self):
parent = self.request.POST.get('parent', None)
if parent:
container, slash, remainder = parent.partition(
swift.FOLDER_DELIMITER)
args = (utils.wrap_delimiter(container),
utils.wrap_delimiter(remainder))
return reverse(self.success_url, args=args)
else:
container = utils.wrap_delimiter(self.request.POST['name'])
return reverse(self.success_url, args=[container])
def get_initial(self):
initial = super(CreateView, self).get_initial()
initial['parent'] = self.kwargs['container_name']
return initial
class CreatePseudoFolderView(forms.ModalFormView):
form_class = project_forms.CreatePseudoFolder
template_name = 'project/containers/create_pseudo_folder.html'
success_url = "horizon:project:containers:index"
page_title = _("Create Pseudo-folder")
def get_success_url(self):
container_name = self.request.POST['container_name']
return reverse(self.success_url,
args=(utils.wrap_delimiter(container_name),
self.request.POST.get('path', '')))
def get_initial(self):
return {"container_name": self.kwargs["container_name"],
"path": self.kwargs['subfolder_path']}
def get_context_data(self, **kwargs):
context = super(CreatePseudoFolderView, self). \
get_context_data(**kwargs)
context['container_name'] = self.kwargs["container_name"]
return context
class UploadView(forms.ModalFormView):
form_class = project_forms.UploadObject
template_name = 'project/containers/upload.html'
success_url = "horizon:project:containers:index"
page_title = _("Upload Objects")
def get_success_url(self):
container = utils.wrap_delimiter(self.request.POST['container_name'])
path = utils.wrap_delimiter(self.request.POST.get('path', ''))
args = (container, path)
return reverse(self.success_url, args=args)
def get_initial(self):
return {"container_name": self.kwargs["container_name"],
"path": self.kwargs['subfolder_path']}
def get_context_data(self, **kwargs):
context = super(UploadView, self).get_context_data(**kwargs)
context['container_name'] = self.kwargs["container_name"]
return context
def object_download(request, container_name, object_path):
try:
obj = api.swift.swift_get_object(request, container_name, object_path,
resp_chunk_size=swift.CHUNK_SIZE)
except Exception:
redirect = reverse("horizon:project:containers:index")
exceptions.handle(request,
_("Unable to retrieve object."),
redirect=redirect)
# Add the original file extension back on if it wasn't preserved in the
# name given to the object.
filename = object_path.rsplit(swift.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.StreamingHttpResponse(obj.data)
safe_name = filename.replace(",", "")
if six.PY2:
safe_name = safe_name.encode('utf-8')
response['Content-Disposition'] = 'attachment; filename="%s"' % safe_name
response['Content-Type'] = 'application/octet-stream'
response['Content-Length'] = obj.bytes
return response
class CopyView(forms.ModalFormView):
form_class = project_forms.CopyObject
template_name = 'project/containers/copy.html'
success_url = "horizon:project:containers:index"
page_title = _("Copy Object")
def get_success_url(self):
container = utils.wrap_delimiter(
self.request.POST['new_container_name'])
path = utils.wrap_delimiter(self.request.POST.get('path', ''))
args = (container, path)
return reverse(self.success_url, args=args)
def get_form_kwargs(self):
kwargs = super(CopyView, self).get_form_kwargs()
try:
containers = api.swift.swift_get_containers(self.request)
except Exception:
redirect = reverse("horizon:project:containers:index")
exceptions.handle(self.request,
_('Unable to list containers.'),
redirect=redirect)
kwargs['containers'] = [(c.name, c.name) for c in containers[0]]
return kwargs
@staticmethod
def get_copy_name(object_name):
filename, ext = os.path.splitext(object_name)
return "%s.copy%s" % (filename, ext)
def get_initial(self):
path = self.kwargs["subfolder_path"]
object_name = self.kwargs["object_name"]
orig = "%s%s" % (path or '', object_name)
return {"new_container_name": self.kwargs["container_name"],
"orig_container_name": self.kwargs["container_name"],
"orig_object_name": orig,
"path": path,
"new_object_name": self.get_copy_name(object_name)}
def get_context_data(self, **kwargs):
context = super(CopyView, self).get_context_data(**kwargs)
context['container_name'] = self.kwargs["container_name"]
context['object_name'] = self.kwargs["object_name"]
return context
class ContainerDetailView(forms.ModalFormMixin, generic.TemplateView):
template_name = 'project/containers/container_detail.html'
page_title = _("Container Details")
@memoized.memoized_method
def get_object(self):
try:
return api.swift.swift_get_container(
self.request,
self.kwargs["container_name"],
with_data=False)
except Exception:
redirect = reverse("horizon:project:containers:index")
exceptions.handle(self.request,
_('Unable to retrieve details.'),
redirect=redirect)
def get_context_data(self, **kwargs):
context = super(ContainerDetailView, self).get_context_data(**kwargs)
context['container'] = self.get_object()
return context
class ObjectDetailView(forms.ModalFormMixin, generic.TemplateView):
template_name = 'project/containers/object_detail.html'
page_title = _("Object Details")
@memoized.memoized_method
def get_object(self):
try:
return api.swift.swift_get_object(
self.request,
self.kwargs["container_name"],
self.kwargs["object_path"],
with_data=False)
except Exception:
redirect = reverse("horizon:project:containers:index")
exceptions.handle(self.request,
_('Unable to retrieve details.'),
redirect=redirect)
def get_context_data(self, **kwargs):
context = super(ObjectDetailView, self).get_context_data(**kwargs)
context['object'] = self.get_object()
return context
class UpdateObjectView(forms.ModalFormView):
form_class = project_forms.UpdateObject
template_name = 'project/containers/update.html'
success_url = "horizon:project:containers:index"
page_title = _("Update Object")
def get_success_url(self):
container = utils.wrap_delimiter(self.request.POST['container_name'])
path = utils.wrap_delimiter(self.request.POST.get('path', ''))
args = (container, path)
return reverse(self.success_url, args=args)
def get_initial(self):
return {"container_name": self.kwargs["container_name"],
"path": self.kwargs["subfolder_path"],
"name": self.kwargs["object_name"]}
def get_context_data(self, **kwargs):
context = super(UpdateObjectView, self).get_context_data(**kwargs)
context['container_name'] = self.kwargs["container_name"]
context['subfolder_path'] = self.kwargs["subfolder_path"]
context['object_name'] = self.kwargs["object_name"]
return context

View File

@ -74,7 +74,7 @@ class MappingsTests(test.TestCase):
'AWS::EC2::Instance',
'aaa')
assertMappingUrl(
'/project/containers/aaa/',
'/project/containers/container/aaa/',
'OS::Swift::Container',
'aaa')
assertMappingUrl(

View File

@ -26,7 +26,7 @@ from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.containers \
import forms as containers_forms
import utils as containers_utils
class CreateBackupForm(forms.SelfHandlingForm):
@ -37,7 +37,7 @@ class CreateBackupForm(forms.SelfHandlingForm):
container_name = forms.CharField(
max_length=255,
label=_("Container Name"),
validators=[containers_forms.no_slash_validator],
validators=[containers_utils.no_slash_validator],
required=False)
volume_id = forms.CharField(widget=forms.HiddenInput())

View File

@ -25,13 +25,6 @@ ADD_PANEL = ('openstack_dashboard.dashboards.project.'
DISABLED = False
# Which implementation of the panel should we use? Valid options
# here are 'angular' (new implementation) and 'legacy' (old
# implementation.)
UPDATE_HORIZON_CONFIG = {
'swift_panel': 'angular'
}
ADD_SCSS_FILES = [
'dashboard/project/containers/_containers.scss',
]

View File

@ -119,10 +119,6 @@ settings_utils.update_dashboards(
INSTALLED_APPS,
)
# Remove this when the legacy panel is removed, along with its tests and
# the stacks MappingsTests are updated with the new URL path.
HORIZON_CONFIG['swift_panel'] = 'legacy'
settings_utils.find_static_files(HORIZON_CONFIG, AVAILABLE_THEMES,
THEME_COLLECTION_DIR, ROOT_PATH)

View File

@ -0,0 +1,8 @@
---
upgrade:
- Any past use of the Django based Swift UI is no longer
supported and the code is being removed. The new
angularJS based version should be used instead.
deprecations:
- Removing formerly deprecated Swift UI code that was
replaced with an improved Angular version in Mitaka.