Allow image filtering based on image ownership
Uses buttons above the images list to allow filtering into categories: project, "official", shared, public. Adds a FixedFilterAction that creates a button group on top of the datatable. Supports server-side and client-side filtering. Implements blueprint organised-images-display. Change-Id: I87f6cf4dd7d7397ad8c23ebadc8bf293d0bad998
This commit is contained in:
parent
efff047b04
commit
62dd7b4e99
@ -218,6 +218,20 @@ horizon.datatables.update_footer_count = function (el, modifier) {
|
||||
$footer.text(footer_text);
|
||||
};
|
||||
|
||||
horizon.datatables.add_no_results_row = function (table) {
|
||||
// Add a "no results" row if there are no results.
|
||||
template = horizon.templates.compiled_templates["#empty_row_template"];
|
||||
if (!table.find("tbody tr:visible").length && typeof(template) !== "undefined") {
|
||||
colspan = table.find("th[colspan]").attr('colspan');
|
||||
params = {"colspan": colspan};
|
||||
table.find("tbody").append(template.render(params));
|
||||
}
|
||||
};
|
||||
|
||||
horizon.datatables.remove_no_results_row = function (table) {
|
||||
table.find("tr.empty").remove();
|
||||
};
|
||||
|
||||
horizon.datatables.set_table_sorting = function (parent) {
|
||||
// Function to initialize the tablesorter plugin strictly on sortable columns.
|
||||
$(parent).find("table.datatable").each(function () {
|
||||
@ -252,7 +266,7 @@ horizon.datatables.add_table_checkboxes = function(parent) {
|
||||
});
|
||||
};
|
||||
|
||||
horizon.datatables.set_table_filter = function (parent) {
|
||||
horizon.datatables.set_table_query_filter = function (parent) {
|
||||
$(parent).find('table').each(function (index, elm) {
|
||||
var input = $($(elm).find('div.table_search input')),
|
||||
table_selector;
|
||||
@ -280,23 +294,14 @@ horizon.datatables.set_table_filter = function (parent) {
|
||||
'show': this.show,
|
||||
'hide': this.hide,
|
||||
onBefore: function () {
|
||||
// Clear the "no results" row.
|
||||
var table = $(table_selector);
|
||||
table.find("tr.empty").remove();
|
||||
horizon.datatables.remove_no_results_row(table);
|
||||
},
|
||||
onAfter: function () {
|
||||
var template, table, colspan, params;
|
||||
table = $(table_selector);
|
||||
horizon.datatables.update_footer_count(table);
|
||||
// Add a "no results" row if there are no results.
|
||||
template = horizon.templates.compiled_templates["#empty_row_template"];
|
||||
if (!$(table_selector + " tbody tr:visible").length && typeof(template) !== "undefined") {
|
||||
colspan = table.find("th[colspan]").attr('colspan');
|
||||
params = {"colspan": colspan};
|
||||
table.find("tbody").append(template.render(params));
|
||||
}
|
||||
// Update footer count
|
||||
|
||||
horizon.datatables.add_no_results_row(table);
|
||||
},
|
||||
prepareQuery: function (val) {
|
||||
return new RegExp(val, "i");
|
||||
@ -309,6 +314,29 @@ horizon.datatables.set_table_filter = function (parent) {
|
||||
});
|
||||
};
|
||||
|
||||
horizon.datatables.set_table_fixed_filter = function (parent) {
|
||||
$(parent).find('table.datatable').each(function (index, elm) {
|
||||
$(elm).on('click', 'div.table_filter button', function(evt) {
|
||||
var table = $(elm);
|
||||
var category = $(this).val();
|
||||
evt.preventDefault();
|
||||
horizon.datatables.remove_no_results_row(table);
|
||||
table.find('tbody tr').hide();
|
||||
table.find('tbody tr.category-' + category).show();
|
||||
horizon.datatables.update_footer_count(table);
|
||||
horizon.datatables.add_no_results_row(table);
|
||||
});
|
||||
$(elm).find('div.table_filter button').each(function (i, button) {
|
||||
// Select the first non-empty category
|
||||
if ($(button).text().indexOf(' (0)') == -1) {
|
||||
$(button).addClass('active');
|
||||
$(button).trigger('click');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
horizon.addInitFunction(function() {
|
||||
horizon.datatables.validate_button();
|
||||
horizon.datatables.update_footer_count($.find('table.datatable'),0);
|
||||
@ -341,12 +369,14 @@ horizon.addInitFunction(function() {
|
||||
// Trigger run-once setup scripts for tables.
|
||||
horizon.datatables.add_table_checkboxes($('body'));
|
||||
horizon.datatables.set_table_sorting($('body'));
|
||||
horizon.datatables.set_table_filter($('body'));
|
||||
horizon.datatables.set_table_query_filter($('body'));
|
||||
horizon.datatables.set_table_fixed_filter($('body'));
|
||||
|
||||
// Also apply on tables in modal views.
|
||||
horizon.modals.addModalInitFunction(horizon.datatables.add_table_checkboxes);
|
||||
horizon.modals.addModalInitFunction(horizon.datatables.set_table_sorting);
|
||||
horizon.modals.addModalInitFunction(horizon.datatables.set_table_filter);
|
||||
horizon.modals.addModalInitFunction(horizon.datatables.set_table_query_filter);
|
||||
horizon.modals.addModalInitFunction(horizon.datatables.set_table_fixed_filter);
|
||||
|
||||
horizon.datatables.update();
|
||||
});
|
||||
|
@ -1,29 +1,49 @@
|
||||
module("Tables (horizon.tables.js)");
|
||||
|
||||
test("Row filtering (fixed)", function () {
|
||||
var fixture = $("#qunit-fixture");
|
||||
var table = fixture.find("#table2");
|
||||
|
||||
ok(!table.find(".cat").is(":hidden"), "Filtering cats: cats visible by default");
|
||||
ok(table.find(":not(.cat)").is(":hidden"), "Filtering cats: non-cats hidden by default");
|
||||
|
||||
$("#button_cats").trigger("click");
|
||||
ok(!table.find(".cat").is(":hidden"), "Filtering cats: cats visible");
|
||||
ok(table.find(":not(.cat)").is(":hidden"), "Filtering cats: non-cats hidden");
|
||||
|
||||
$("#button_dogs").trigger("click");
|
||||
ok(!table.find(".dog").is(":hidden"), "Filtering dogs: dogs visible");
|
||||
ok(table.find(":not(.dog)").is(":hidden"), "Filtering dogs: non-dogs hidden");
|
||||
|
||||
$("#button_big").trigger("click");
|
||||
ok(!table.find(".big").is(":hidden"), "Filtering big animals: big visible");
|
||||
ok(table.find(":not(.big)").is(":hidden"), "Filtering big animals: non-big hidden");
|
||||
});
|
||||
|
||||
test("Footer count update", function () {
|
||||
var fixture = $("#qunit-fixture");
|
||||
var table = fixture.find("table.datatable");
|
||||
var table = fixture.find("#table1");
|
||||
var tbody = table.find('tbody');
|
||||
var table_count = table.find("span.table_count");
|
||||
var rows = tbody.find('tr');
|
||||
|
||||
horizon.datatables.update_footer_count(table);
|
||||
notEqual(table_count.text().indexOf('4 items'), -1, "Initial count is correct");
|
||||
|
||||
// hide rows
|
||||
$("table.datatable tbody tr#dog1").hide();
|
||||
$("table.datatable tbody tr#cat2").hide();
|
||||
rows.first().hide();
|
||||
rows.first().next().hide();
|
||||
horizon.datatables.update_footer_count(table);
|
||||
notEqual(table_count.text().indexOf('2 items'), -1, "Count correct after hiding two rows");
|
||||
|
||||
// show a row
|
||||
$("table.datatable tbody tr#cat2").show();
|
||||
rows.first().next().show();
|
||||
horizon.datatables.update_footer_count(table);
|
||||
notEqual(table_count.text().indexOf('3 items'), -1, "Count correct after showing one row");
|
||||
|
||||
// add rows
|
||||
$("table.datatable tbody tr#cat2").show();
|
||||
$('<tr id="cat3"><td>cat3</td></tr>"').appendTo(tbody);
|
||||
$('<tr id="cat4"><td>cat4</td></tr>"').appendTo(tbody);
|
||||
$('<tr><td>cat3</td></tr>"').appendTo(tbody);
|
||||
$('<tr><td>cat4</td></tr>"').appendTo(tbody);
|
||||
horizon.datatables.update_footer_count(table);
|
||||
notEqual(table_count.text().indexOf('5 items'), -1, "Count correct after adding two rows");
|
||||
});
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
# Convenience imports for public API components.
|
||||
from .actions import (Action, BatchAction, DeleteAction,
|
||||
LinkAction, FilterAction)
|
||||
LinkAction, FilterAction, FixedFilterAction)
|
||||
from .base import DataTable, Column, Row
|
||||
from .views import DataTableView, MultiTableView, MultiTableMixin, \
|
||||
MixedDataTableView
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import logging
|
||||
import new
|
||||
from collections import defaultdict
|
||||
|
||||
from django import shortcuts
|
||||
from django.conf import settings
|
||||
@ -328,6 +329,16 @@ class FilterAction(BaseAction):
|
||||
|
||||
A string representing the name of the request parameter used for the
|
||||
search term. Default: ``"q"``.
|
||||
|
||||
.. attribute: filter_type
|
||||
|
||||
A string representing the type of this filter. Default: ``"query"``.
|
||||
|
||||
.. attribute: needs_preloading
|
||||
|
||||
If True, the filter function will be called for the initial
|
||||
GET request with an empty ``filter_string``, regardless of the
|
||||
value of ``method``.
|
||||
"""
|
||||
# TODO(gabriel): The method for a filter action should be a GET,
|
||||
# but given the form structure of the table that's currently impossible.
|
||||
@ -336,6 +347,8 @@ class FilterAction(BaseAction):
|
||||
method = "POST"
|
||||
name = "filter"
|
||||
verbose_name = _("Filter")
|
||||
filter_type = "query"
|
||||
needs_preloading = False
|
||||
|
||||
def __init__(self, verbose_name=None, param_name=None):
|
||||
super(FilterAction, self).__init__()
|
||||
@ -387,6 +400,52 @@ class FilterAction(BaseAction):
|
||||
"implemented by %s." % self.__class__)
|
||||
|
||||
|
||||
class FixedFilterAction(FilterAction):
|
||||
""" A filter action with fixed buttons.
|
||||
"""
|
||||
filter_type = 'fixed'
|
||||
needs_preloading = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FixedFilterAction, self).__init__(args, kwargs)
|
||||
self.fixed_buttons = self.get_fixed_buttons()
|
||||
self.filter_string = ''
|
||||
|
||||
def filter(self, table, images, filter_string):
|
||||
self.filter_string = filter_string
|
||||
categories = self.categorize(table, images)
|
||||
self.categories = defaultdict(list, categories)
|
||||
for button in self.fixed_buttons:
|
||||
button['count'] = len(self.categories[button['value']])
|
||||
if not filter_string:
|
||||
return images
|
||||
return self.categories[filter_string]
|
||||
|
||||
def get_fixed_buttons(self):
|
||||
"""Returns a list of dictionaries describing the fixed buttons
|
||||
to use for filtering.
|
||||
|
||||
Each list item should be a dict with the keys:
|
||||
text: Text to display on the button
|
||||
icon: Icon class for icon element (inserted before text).
|
||||
value: Value returned when the button is clicked.
|
||||
This value is passed to ``filter()`` as
|
||||
``filter_string``.
|
||||
"""
|
||||
raise NotImplementedError("The get_fixed_buttons method has "
|
||||
"not been implemented by %s." %
|
||||
self.__class__)
|
||||
|
||||
def categorize(self, table, images):
|
||||
"""Override to separate images into categories.
|
||||
|
||||
Return a dict with a key for the value of each fixed button,
|
||||
and a value that is a list of images in that category.
|
||||
"""
|
||||
raise NotImplementedError("The categorize method has not been "
|
||||
"implemented by %s." % self.__class__)
|
||||
|
||||
|
||||
class BatchAction(Action):
|
||||
""" A table action which takes batch action on one or more
|
||||
objects. This action should not require user input on a
|
||||
|
@ -950,7 +950,11 @@ class DataTable(object):
|
||||
action = self._meta._filter_action
|
||||
filter_string = self.get_filter_string()
|
||||
request_method = self.request.method
|
||||
if filter_string and request_method == action.method:
|
||||
needs_preloading = (not filter_string
|
||||
and request_method == 'GET'
|
||||
and action.needs_preloading)
|
||||
valid_method = (request_method == action.method)
|
||||
if (filter_string and valid_method) or needs_preloading:
|
||||
if self._meta.mixed_data_type:
|
||||
self._filtered_data = action.data_type_filter(self,
|
||||
self.data,
|
||||
|
@ -1,9 +1,15 @@
|
||||
<div class="table_actions clearfix">
|
||||
{% if filter %}
|
||||
<div class="table_search">
|
||||
<input class="span3 example" value="{{ filter.filter_string|default:'' }}" type="text" name="{{ filter.get_param_name }}" />
|
||||
<button type="submit" {{ filter.attr_string|safe }}>Filter</button>
|
||||
</div>
|
||||
{% if filter.filter_type == 'fixed' %}
|
||||
<div class="table_filter btn-group" data-toggle="buttons-radio">
|
||||
{% for button in filter.fixed_buttons %}
|
||||
<button name="{{ filter.get_param_name }}" type="submit" value="{{ button.value }}" class="btn btn-small{% ifequal button.value filter.filter_string %} active{% endifequal %}">{% if button.icon %}<i class="{{ button.icon }}"></i> {% endif %}{{ button.text }}{% if button.count >= 0 %} ({{ button.count }}){% endif %}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif filter.filter_type == 'query' %}
|
||||
<div class="table_search">
|
||||
<input class="span3 example" value="{{ filter.filter_string|default:'' }}" type="text" name="{{ filter.get_param_name }}" />
|
||||
<button type="submit" {{ filter.attr_string|safe }}>Filter</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for action in table_actions %}
|
||||
{% if action != filter %}
|
||||
|
@ -33,10 +33,10 @@
|
||||
|
||||
<table id="table1" class="datatable">
|
||||
<tbody>
|
||||
<tr id="cat1"><td>cat1</td></tr>
|
||||
<tr id="dog1"><td>dog1</td></tr>
|
||||
<tr id="cat2"><td>cat2</td></tr>
|
||||
<tr id="dog2"><td>dog2</td></tr>
|
||||
<tr><td>cat1</td></tr>
|
||||
<tr><td>dog1</td></tr>
|
||||
<tr><td>cat2</td></tr>
|
||||
<tr><td>dog2</td></tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
@ -46,6 +46,28 @@
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<table id="table2" class="datatable">
|
||||
<thead><tr><th><div class="table_filter" data-toggle="buttons-radio">
|
||||
<button name="cats" type="submit" value="cats" id="button_cats">Cats</button>
|
||||
<button name="dogs" type="submit" value="dogs" id="button_dogs">Dogs</button>
|
||||
<button name="big" type="submit" value="big" id="button_big">Big Animals</button>
|
||||
</div></th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="category-cat"><td>cat1</td></tr>
|
||||
<tr class="category-big category-dog"><td>dog1</td></tr>
|
||||
<tr class="category-big category-cat"><td>cat2</td></tr>
|
||||
<tr class="category-dog"><td>dog2</td></tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<span class="table_count">Displaying 4 items</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -48,21 +48,23 @@ class ImagesViewTest(test.BaseAdminViewTests):
|
||||
|
||||
@test.create_stubs({api.glance: ('image_list_detailed',)})
|
||||
def test_images_list_get_pagination(self):
|
||||
images = self.images.list()[:5]
|
||||
|
||||
api.glance.image_list_detailed(IsA(http.HttpRequest),
|
||||
marker=None) \
|
||||
.AndReturn([self.images.list(),
|
||||
.AndReturn([images,
|
||||
True])
|
||||
api.glance.image_list_detailed(IsA(http.HttpRequest),
|
||||
marker=None) \
|
||||
.AndReturn([self.images.list()[:2],
|
||||
.AndReturn([images[:2],
|
||||
True])
|
||||
api.glance.image_list_detailed(IsA(http.HttpRequest),
|
||||
marker=self.images.list()[2].id) \
|
||||
.AndReturn([self.images.list()[2:4],
|
||||
marker=images[2].id) \
|
||||
.AndReturn([images[2:4],
|
||||
True])
|
||||
api.glance.image_list_detailed(IsA(http.HttpRequest),
|
||||
marker=self.images.list()[4].id) \
|
||||
.AndReturn([self.images.list()[4:],
|
||||
marker=images[4].id) \
|
||||
.AndReturn([images[4:],
|
||||
True])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -70,7 +72,7 @@ class ImagesViewTest(test.BaseAdminViewTests):
|
||||
res = self.client.get(url)
|
||||
# get all
|
||||
self.assertEqual(len(res.context['images_table'].data),
|
||||
len(self.images.list()))
|
||||
len(images))
|
||||
self.assertTemplateUsed(res, 'admin/images/index.html')
|
||||
|
||||
page_size = getattr(settings, "API_RESULT_PAGE_SIZE", None)
|
||||
@ -83,7 +85,7 @@ class ImagesViewTest(test.BaseAdminViewTests):
|
||||
|
||||
url = "?".join([reverse('horizon:admin:images:index'),
|
||||
"=".join([AdminImagesTable._meta.pagination_param,
|
||||
self.images.list()[2].id])])
|
||||
images[2].id])])
|
||||
res = self.client.get(url)
|
||||
# get second page (items 2-4)
|
||||
self.assertEqual(len(res.context['images_table'].data),
|
||||
@ -91,7 +93,7 @@ class ImagesViewTest(test.BaseAdminViewTests):
|
||||
|
||||
url = "?".join([reverse('horizon:admin:images:index'),
|
||||
"=".join([AdminImagesTable._meta.pagination_param,
|
||||
self.images.list()[4].id])])
|
||||
images[4].id])])
|
||||
res = self.client.get(url)
|
||||
# get third page (item 5)
|
||||
self.assertEqual(len(res.context['images_table'].data),
|
||||
|
@ -15,13 +15,16 @@
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template import defaultfilters as filters
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
from horizon.utils.memoized import memoized
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
@ -78,6 +81,52 @@ class EditImage(tables.LinkAction):
|
||||
return False
|
||||
|
||||
|
||||
def filter_tenants():
|
||||
return getattr(settings, 'IMAGES_LIST_FILTER_TENANTS', [])
|
||||
|
||||
|
||||
@memoized
|
||||
def filter_tenant_ids():
|
||||
return map(lambda ft: ft['tenant'], filter_tenants())
|
||||
|
||||
|
||||
class OwnerFilter(tables.FixedFilterAction):
|
||||
def get_fixed_buttons(self):
|
||||
def make_dict(text, tenant, icon):
|
||||
return dict(text=text, value=tenant, icon=icon)
|
||||
|
||||
buttons = [make_dict('Project', 'project', 'icon-home')]
|
||||
for button_dict in filter_tenants():
|
||||
new_dict = button_dict.copy()
|
||||
new_dict['value'] = new_dict['tenant']
|
||||
buttons.append(new_dict)
|
||||
buttons.append(make_dict('Shared with Me', 'shared', 'icon-share'))
|
||||
buttons.append(make_dict('Public', 'public', 'icon-fire'))
|
||||
return buttons
|
||||
|
||||
def categorize(self, table, images):
|
||||
user_tenant_id = table.request.user.tenant_id
|
||||
tenants = defaultdict(list)
|
||||
for im in images:
|
||||
categories = get_image_categories(im, user_tenant_id)
|
||||
for category in categories:
|
||||
tenants[category].append(im)
|
||||
return tenants
|
||||
|
||||
|
||||
def get_image_categories(im, user_tenant_id):
|
||||
categories = []
|
||||
if im.is_public:
|
||||
categories.append('public')
|
||||
if im.owner == user_tenant_id:
|
||||
categories.append('project')
|
||||
elif im.owner in filter_tenant_ids():
|
||||
categories.append(im.owner)
|
||||
elif not im.is_public:
|
||||
categories.append('shared')
|
||||
return categories
|
||||
|
||||
|
||||
def get_image_type(image):
|
||||
return getattr(image, "properties", {}).get("image_type", _("Image"))
|
||||
|
||||
@ -97,6 +146,15 @@ class UpdateRow(tables.Row):
|
||||
image = api.glance.image_get(request, image_id)
|
||||
return image
|
||||
|
||||
def load_cells(self, image=None):
|
||||
super(UpdateRow, self).load_cells(image)
|
||||
# Tag the row with the image category for client-side filtering.
|
||||
image = self.datum
|
||||
my_tenant_id = self.table.request.user.tenant_id
|
||||
image_categories = get_image_categories(image, my_tenant_id)
|
||||
for category in image_categories:
|
||||
self.classes.append('category-' + category)
|
||||
|
||||
|
||||
class ImagesTable(tables.DataTable):
|
||||
STATUS_CHOICES = (
|
||||
@ -133,6 +191,6 @@ class ImagesTable(tables.DataTable):
|
||||
# Hide the image_type column. Done this way so subclasses still get
|
||||
# all the columns by default.
|
||||
columns = ["name", "status", "public", "disk_format"]
|
||||
table_actions = (CreateImage, DeleteImage,)
|
||||
table_actions = (OwnerFilter, CreateImage, DeleteImage,)
|
||||
row_actions = (LaunchImage, EditImage, DeleteImage,)
|
||||
pagination_param = "image_marker"
|
||||
|
@ -19,13 +19,18 @@
|
||||
# under the License.
|
||||
|
||||
from django import http
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from mox import IsA
|
||||
|
||||
from horizon import tables as horizon_tables
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
from . import tables
|
||||
|
||||
|
||||
IMAGES_INDEX_URL = reverse('horizon:project:images_and_snapshots:index')
|
||||
|
||||
@ -116,3 +121,48 @@ class ImageViewTests(test.TestCase):
|
||||
" name='public' checked='checked'>",
|
||||
html=True,
|
||||
msg_prefix="The is_public checkbox is not checked")
|
||||
|
||||
|
||||
class OwnerFilterTests(test.TestCase):
|
||||
def setUp(self):
|
||||
super(OwnerFilterTests, self).setUp()
|
||||
self.table = self.mox.CreateMock(horizon_tables.DataTable)
|
||||
self.table.request = self.request
|
||||
|
||||
@override_settings(IMAGES_LIST_FILTER_TENANTS=[{'name': 'Official',
|
||||
'tenant': 'officialtenant',
|
||||
'icon': 'icon-ok'}])
|
||||
def test_filter(self):
|
||||
self.mox.ReplayAll()
|
||||
all_images = self.images.list()
|
||||
table = self.table
|
||||
self.filter_tenants = settings.IMAGES_LIST_FILTER_TENANTS
|
||||
|
||||
filter_ = tables.OwnerFilter()
|
||||
|
||||
images = filter_.filter(table, all_images, 'project')
|
||||
self.assertEqual(images, self._expected('project'))
|
||||
|
||||
images = filter_.filter(table, all_images, 'public')
|
||||
self.assertEqual(images, self._expected('public'))
|
||||
|
||||
images = filter_.filter(table, all_images, 'shared')
|
||||
self.assertEqual(images, self._expected('shared'))
|
||||
|
||||
images = filter_.filter(table, all_images, 'officialtenant')
|
||||
self.assertEqual(images, self._expected('officialtenant'))
|
||||
|
||||
def _expected(self, filter_string):
|
||||
my_tenant_id = self.request.user.tenant_id
|
||||
images = self.images.list()
|
||||
special = map(lambda t: t['tenant'], self.filter_tenants)
|
||||
|
||||
if filter_string == 'public':
|
||||
return filter(lambda im: im.is_public, images)
|
||||
if filter_string == 'shared':
|
||||
return filter(lambda im: not im.is_public and
|
||||
im.owner != my_tenant_id and
|
||||
im.owner not in special, images)
|
||||
if filter_string == 'project':
|
||||
filter_string = my_tenant_id
|
||||
return filter(lambda im: im.owner == filter_string, images)
|
||||
|
@ -545,7 +545,7 @@ table form {
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.table_actions .table_search {
|
||||
.table_actions .table_search, .table_actions .table_filter {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@ -568,11 +568,20 @@ table form {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.table_header .table_actions a, .table_header .table_actions button {
|
||||
.table_header .table_actions a, .table_header .table_actions > button {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.table_header .table_filter {
|
||||
vertical-align: bottom;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.table_header .table_filter i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table_actions form {
|
||||
float: right;
|
||||
margin-left: 10px;
|
||||
|
@ -27,19 +27,22 @@ def data(TEST):
|
||||
'id': 3,
|
||||
'status': "active",
|
||||
'owner': TEST.tenant.id,
|
||||
'properties': {'image_type': u'snapshot'}}
|
||||
'properties': {'image_type': u'snapshot'},
|
||||
'is_public': False}
|
||||
snapshot_dict_no_owner = {'name': u'snapshot 2',
|
||||
'container_format': u'ami',
|
||||
'id': 4,
|
||||
'status': "active",
|
||||
'owner': None,
|
||||
'properties': {'image_type': u'snapshot'}}
|
||||
'properties': {'image_type': u'snapshot'},
|
||||
'is_public': False}
|
||||
snapshot_dict_queued = {'name': u'snapshot 2',
|
||||
'container_format': u'ami',
|
||||
'id': 5,
|
||||
'status': "queued",
|
||||
'owner': TEST.tenant.id,
|
||||
'properties': {'image_type': u'snapshot'}}
|
||||
'properties': {'image_type': u'snapshot'},
|
||||
'is_public': False}
|
||||
snapshot = Image(ImageManager(None), snapshot_dict)
|
||||
TEST.snapshots.add(snapshot)
|
||||
snapshot = Image(ImageManager(None), snapshot_dict_no_owner)
|
||||
@ -53,14 +56,16 @@ def data(TEST):
|
||||
'status': "active",
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'novaImage',
|
||||
'properties': {'image_type': u'image'}}
|
||||
'properties': {'image_type': u'image'},
|
||||
'is_public': True}
|
||||
public_image = Image(ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': 'a001c047-22f8-47d0-80a1-8ec94a9524fe',
|
||||
'name': 'private_image',
|
||||
'status': "active",
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'aki'}
|
||||
'container_format': 'aki',
|
||||
'is_public': False}
|
||||
private_image = Image(ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': '278905a6-4b52-4d1e-98f9-8c57bb25ba32',
|
||||
@ -68,22 +73,45 @@ def data(TEST):
|
||||
'status': "active",
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'novaImage',
|
||||
'properties': {'image_type': u'image'}}
|
||||
'properties': {'image_type': u'image'},
|
||||
'is_public': True}
|
||||
public_image2 = Image(ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': '710a1acf-a3e3-41dd-a32d-5d6b6c86ea10',
|
||||
'name': 'private_image 2',
|
||||
'status': "active",
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'aki'}
|
||||
'container_format': 'aki',
|
||||
'is_public': False}
|
||||
private_image2 = Image(ImageManager(None), image_dict)
|
||||
|
||||
image_dict = {'id': '7cd892fd-5652-40f3-a450-547615680132',
|
||||
'name': 'private_image 3',
|
||||
'status': "active",
|
||||
'owner': TEST.tenant.id,
|
||||
'container_format': 'aki'}
|
||||
'container_format': 'aki',
|
||||
'is_public': False}
|
||||
private_image3 = Image(ImageManager(None), image_dict)
|
||||
|
||||
# A shared image. Not public and not local tenant.
|
||||
image_dict = {'id': 'c8756975-7a3b-4e43-b7f7-433576112849',
|
||||
'name': 'shared_image 1',
|
||||
'status': "active",
|
||||
'owner': 'someothertenant',
|
||||
'container_format': 'aki',
|
||||
'is_public': False}
|
||||
shared_image1 = Image(ImageManager(None), image_dict)
|
||||
|
||||
# "Official" image. Public and tenant matches an entry
|
||||
# in IMAGES_LIST_FILTER_TENANTS.
|
||||
image_dict = {'id': 'f448704f-0ce5-4d34-8441-11b6581c6619',
|
||||
'name': 'official_image 1',
|
||||
'status': "active",
|
||||
'owner': 'officialtenant',
|
||||
'container_format': 'aki',
|
||||
'is_public': True}
|
||||
official_image1 = Image(ImageManager(None), image_dict)
|
||||
|
||||
TEST.images.add(public_image, private_image,
|
||||
public_image2, private_image2, private_image3)
|
||||
public_image2, private_image2, private_image3,
|
||||
shared_image1, official_image1)
|
||||
|
Loading…
x
Reference in New Issue
Block a user