Makes data table accept mixed data types
This is the first step of making the resource browser, related to blueprint swift-ui-improvements. Adds three attrs to DataTableOptions: .. attribute:: mixed_data_type .. attribute:: data_types .. attribute:: data_type_name Adds attr to Column class: .. attribute:: allowed_data_types Adds attr to Action and LinkAction class: .. attribute:: allowed_data_types Changes implentation of FilterAction. Adds MixedDataTableView. Change-Id: I45fce5e889e92abc592f0a5abfef1abb4ce527be
This commit is contained in:
parent
576262a142
commit
6ba1a4d395
@ -118,8 +118,6 @@ def swift_filter_objects(request, filter_string, container_name, prefix=None,
|
|||||||
filter_string_list = filter_string.lower().strip().split(' ')
|
filter_string_list = filter_string.lower().strip().split(' ')
|
||||||
|
|
||||||
def matches_filter(obj):
|
def matches_filter(obj):
|
||||||
if obj.content_type == "application/directory":
|
|
||||||
return False
|
|
||||||
for q in filter_string_list:
|
for q in filter_string_list:
|
||||||
return wildcard_search(obj.name.lower(), q)
|
return wildcard_search(obj.name.lower(), q)
|
||||||
|
|
||||||
|
@ -107,8 +107,10 @@ class ContainersTable(tables.DataTable):
|
|||||||
|
|
||||||
|
|
||||||
class DeleteObject(tables.DeleteAction):
|
class DeleteObject(tables.DeleteAction):
|
||||||
|
name = "delete_object"
|
||||||
data_type_singular = _("Object")
|
data_type_singular = _("Object")
|
||||||
data_type_plural = _("Objects")
|
data_type_plural = _("Objects")
|
||||||
|
allowed_data_types = ("objects",)
|
||||||
|
|
||||||
def delete(self, request, obj_id):
|
def delete(self, request, obj_id):
|
||||||
obj = self.table.get_object_by_id(obj_id)
|
obj = self.table.get_object_by_id(obj_id)
|
||||||
@ -116,11 +118,26 @@ class DeleteObject(tables.DeleteAction):
|
|||||||
api.swift_delete_object(request, container_name, obj_id)
|
api.swift_delete_object(request, container_name, obj_id)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteSubfolder(DeleteObject):
|
||||||
|
name = "delete_subfolder"
|
||||||
|
data_type_singular = _("Folder")
|
||||||
|
data_type_plural = _("Folders")
|
||||||
|
allowed_data_types = ("subfolders",)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteMultipleObjects(DeleteObject):
|
||||||
|
name = "delete_multiple_objects"
|
||||||
|
data_type_singular = _("Object/Folder")
|
||||||
|
data_type_plural = _("Objects/Folders")
|
||||||
|
allowed_data_types = ("subfolders", "objects",)
|
||||||
|
|
||||||
|
|
||||||
class CopyObject(tables.LinkAction):
|
class CopyObject(tables.LinkAction):
|
||||||
name = "copy"
|
name = "copy"
|
||||||
verbose_name = _("Copy")
|
verbose_name = _("Copy")
|
||||||
url = "horizon:nova:containers:object_copy"
|
url = "horizon:nova:containers:object_copy"
|
||||||
classes = ("ajax-modal", "btn-copy")
|
classes = ("ajax-modal", "btn-copy")
|
||||||
|
allowed_data_types = ("objects",)
|
||||||
|
|
||||||
def get_link_url(self, obj):
|
def get_link_url(self, obj):
|
||||||
return reverse(self.url, args=(http.urlquote(obj.container.name),
|
return reverse(self.url, args=(http.urlquote(obj.container.name),
|
||||||
@ -132,6 +149,7 @@ class DownloadObject(tables.LinkAction):
|
|||||||
verbose_name = _("Download")
|
verbose_name = _("Download")
|
||||||
url = "horizon:nova:containers:object_download"
|
url = "horizon:nova:containers:object_download"
|
||||||
classes = ("btn-download",)
|
classes = ("btn-download",)
|
||||||
|
allowed_data_types = ("objects",)
|
||||||
|
|
||||||
def get_link_url(self, obj):
|
def get_link_url(self, obj):
|
||||||
return reverse(self.url, args=(http.urlquote(obj.container.name),
|
return reverse(self.url, args=(http.urlquote(obj.container.name),
|
||||||
@ -139,39 +157,34 @@ class DownloadObject(tables.LinkAction):
|
|||||||
|
|
||||||
|
|
||||||
class ObjectFilterAction(tables.FilterAction):
|
class ObjectFilterAction(tables.FilterAction):
|
||||||
def filter(self, table, objects, filter_string):
|
def _filtered_data(self, table, filter_string):
|
||||||
request = table._meta.request
|
request = table._meta.request
|
||||||
container = self.table.kwargs['container_name']
|
container = self.table.kwargs['container_name']
|
||||||
subfolder = self.table.kwargs['subfolder_path']
|
subfolder = self.table.kwargs['subfolder_path']
|
||||||
path = subfolder + '/' if subfolder else ''
|
path = subfolder + '/' if subfolder else ''
|
||||||
return api.swift_filter_objects(request,
|
self.filtered_data = api.swift_filter_objects(request,
|
||||||
filter_string,
|
filter_string,
|
||||||
container,
|
container,
|
||||||
path=path)
|
path=path)
|
||||||
|
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/directory"]
|
||||||
|
|
||||||
|
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/directory"]
|
||||||
|
|
||||||
|
|
||||||
def sanitize_name(name):
|
def sanitize_name(name):
|
||||||
return name.split("/")[-1]
|
return name.split("/")[-1]
|
||||||
|
|
||||||
|
|
||||||
class ObjectsTable(tables.DataTable):
|
def get_size(obj):
|
||||||
name = tables.Column("name",
|
return filesizeformat(obj.size)
|
||||||
verbose_name=_("Object Name"),
|
|
||||||
filters=(sanitize_name,))
|
|
||||||
size = tables.Column("size",
|
|
||||||
verbose_name=_('Size'),
|
|
||||||
filters=(filesizeformat,),
|
|
||||||
summation="sum",
|
|
||||||
attrs={'data-type': 'size'})
|
|
||||||
|
|
||||||
def get_object_id(self, obj):
|
|
||||||
return obj.name
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
name = "objects"
|
|
||||||
verbose_name = _("Objects")
|
|
||||||
table_actions = (ObjectFilterAction, UploadObject, DeleteObject)
|
|
||||||
row_actions = (DownloadObject, CopyObject, DeleteObject)
|
|
||||||
|
|
||||||
|
|
||||||
def get_link_subfolder(subfolder):
|
def get_link_subfolder(subfolder):
|
||||||
@ -192,22 +205,22 @@ class CreateSubfolder(CreateContainer):
|
|||||||
return reverse(self.url, args=(http.urlquote(parent + "/"),))
|
return reverse(self.url, args=(http.urlquote(parent + "/"),))
|
||||||
|
|
||||||
|
|
||||||
class DeleteSubfolder(DeleteObject):
|
class ObjectsTable(tables.DataTable):
|
||||||
data_type_singular = _("Folder")
|
|
||||||
data_type_plural = _("Folders")
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerSubfoldersTable(tables.DataTable):
|
|
||||||
name = tables.Column("name",
|
name = tables.Column("name",
|
||||||
link=get_link_subfolder,
|
link=get_link_subfolder,
|
||||||
verbose_name=_("Subfolder Name"),
|
allowed_data_types=("subfolders",),
|
||||||
filters=(sanitize_name,))
|
verbose_name=_("Object Name"),
|
||||||
|
filters=(sanitize_name,))
|
||||||
|
size = tables.Column(get_size, verbose_name=_('Size'))
|
||||||
|
|
||||||
def get_object_id(self, obj):
|
def get_object_id(self, obj):
|
||||||
return obj.name
|
return obj.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
name = "subfolders"
|
name = "objects"
|
||||||
verbose_name = _("Subfolders")
|
verbose_name = _("Subfolders and Objects")
|
||||||
table_actions = (CreateSubfolder, DeleteSubfolder)
|
table_actions = (ObjectFilterAction, CreateSubfolder,
|
||||||
row_actions = (DeleteSubfolder,)
|
UploadObject, DeleteMultipleObjects)
|
||||||
|
row_actions = (DownloadObject, CopyObject, DeleteObject,
|
||||||
|
DeleteSubfolder)
|
||||||
|
data_types = ("subfolders", "objects")
|
||||||
|
@ -179,7 +179,7 @@ class ObjectViewTests(test.TestCase):
|
|||||||
obj.name)
|
obj.name)
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
action_string = "objects__delete__%s" % obj.name
|
action_string = "objects__delete_object__%s" % obj.name
|
||||||
form_data = {"action": action_string}
|
form_data = {"action": action_string}
|
||||||
req = self.factory.post(index_url, form_data)
|
req = self.factory.post(index_url, form_data)
|
||||||
kwargs = {"container_name": container.name}
|
kwargs = {"container_name": container.name}
|
||||||
|
@ -33,8 +33,7 @@ from horizon import exceptions
|
|||||||
from horizon import forms
|
from horizon import forms
|
||||||
from horizon import tables
|
from horizon import tables
|
||||||
from .forms import CreateContainer, UploadObject, CopyObject
|
from .forms import CreateContainer, UploadObject, CopyObject
|
||||||
from .tables import ContainersTable, ObjectsTable,\
|
from .tables import ContainersTable, ObjectsTable
|
||||||
ContainerSubfoldersTable
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -81,8 +80,8 @@ class CreateView(forms.ModalFormView):
|
|||||||
return initial
|
return initial
|
||||||
|
|
||||||
|
|
||||||
class ObjectIndexView(tables.MultiTableView):
|
class ObjectIndexView(tables.MixedDataTableView):
|
||||||
table_classes = (ObjectsTable, ContainerSubfoldersTable)
|
table_class = ObjectsTable
|
||||||
template_name = 'nova/containers/detail.html'
|
template_name = 'nova/containers/detail.html'
|
||||||
|
|
||||||
def has_more_data(self, table):
|
def has_more_data(self, table):
|
||||||
@ -110,6 +109,7 @@ class ObjectIndexView(tables.MultiTableView):
|
|||||||
marker=marker,
|
marker=marker,
|
||||||
path=prefix)
|
path=prefix)
|
||||||
except:
|
except:
|
||||||
|
self._more = None
|
||||||
objects = []
|
objects = []
|
||||||
msg = _('Unable to retrieve object list.')
|
msg = _('Unable to retrieve object list.')
|
||||||
exceptions.handle(self.request, msg)
|
exceptions.handle(self.request, msg)
|
||||||
|
@ -18,4 +18,5 @@
|
|||||||
from .actions import (Action, BatchAction, DeleteAction,
|
from .actions import (Action, BatchAction, DeleteAction,
|
||||||
LinkAction, FilterAction)
|
LinkAction, FilterAction)
|
||||||
from .base import DataTable, Column, Row
|
from .base import DataTable, Column, Row
|
||||||
from .views import DataTableView, MultiTableView, MultiTableMixin
|
from .views import DataTableView, MultiTableView, MultiTableMixin, \
|
||||||
|
MixedDataTableView
|
||||||
|
@ -46,6 +46,21 @@ class BaseAction(html.HTMLElement):
|
|||||||
super(BaseAction, self).__init__()
|
super(BaseAction, self).__init__()
|
||||||
self.datum = datum
|
self.datum = datum
|
||||||
|
|
||||||
|
def data_type_matched(self, datum):
|
||||||
|
""" Method to see if the action is allowed for a certain type of data.
|
||||||
|
Only affects mixed data type tables.
|
||||||
|
"""
|
||||||
|
if datum:
|
||||||
|
action_data_types = getattr(self, "allowed_data_types", [])
|
||||||
|
# If the data types of this action is empty, we assume it accepts
|
||||||
|
# all kinds of data and this method will return True.
|
||||||
|
if action_data_types:
|
||||||
|
datum_type = getattr(datum, self.table._meta.data_type_name,
|
||||||
|
None)
|
||||||
|
if datum_type and (datum_type not in action_data_types):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
def allowed(self, request, datum):
|
||||||
""" Determine whether this action is allowed for the current request.
|
""" Determine whether this action is allowed for the current request.
|
||||||
|
|
||||||
@ -130,6 +145,15 @@ class Action(BaseAction):
|
|||||||
to bypass any API calls and processing which would otherwise be
|
to bypass any API calls and processing which would otherwise be
|
||||||
required to load the table.
|
required to load the table.
|
||||||
|
|
||||||
|
.. attribute:: allowed_data_types
|
||||||
|
|
||||||
|
A list that contains the allowed data types of the action. If the
|
||||||
|
datum's type is in this list, the action will be shown on the row
|
||||||
|
for the datum.
|
||||||
|
|
||||||
|
Default to be an empty list (``[]``). When set to empty, the action
|
||||||
|
will accept any kind of data.
|
||||||
|
|
||||||
At least one of the following methods must be defined:
|
At least one of the following methods must be defined:
|
||||||
|
|
||||||
.. method:: single(self, data_table, request, object_id)
|
.. method:: single(self, data_table, request, object_id)
|
||||||
@ -154,7 +178,7 @@ class Action(BaseAction):
|
|||||||
def __init__(self, verbose_name=None, verbose_name_plural=None,
|
def __init__(self, verbose_name=None, verbose_name_plural=None,
|
||||||
single_func=None, multiple_func=None, handle_func=None,
|
single_func=None, multiple_func=None, handle_func=None,
|
||||||
handles_multiple=False, attrs=None, requires_input=True,
|
handles_multiple=False, attrs=None, requires_input=True,
|
||||||
datum=None):
|
allowed_data_types=[], datum=None):
|
||||||
super(Action, self).__init__(datum=datum)
|
super(Action, self).__init__(datum=datum)
|
||||||
# Priority: constructor, class-defined, fallback
|
# Priority: constructor, class-defined, fallback
|
||||||
self.verbose_name = verbose_name or getattr(self, 'verbose_name',
|
self.verbose_name = verbose_name or getattr(self, 'verbose_name',
|
||||||
@ -168,6 +192,9 @@ class Action(BaseAction):
|
|||||||
self.requires_input = getattr(self,
|
self.requires_input = getattr(self,
|
||||||
"requires_input",
|
"requires_input",
|
||||||
requires_input)
|
requires_input)
|
||||||
|
self.allowed_data_types = getattr(self, "allowed_data_types",
|
||||||
|
allowed_data_types)
|
||||||
|
|
||||||
if attrs:
|
if attrs:
|
||||||
self.attrs.update(attrs)
|
self.attrs.update(attrs)
|
||||||
|
|
||||||
@ -229,19 +256,31 @@ class LinkAction(BaseAction):
|
|||||||
A string or a callable which resolves to a url to be used as the link
|
A string or a callable which resolves to a url to be used as the link
|
||||||
target. You must either define the ``url`` attribute or a override
|
target. You must either define the ``url`` attribute or a override
|
||||||
the ``get_link_url`` method on the class.
|
the ``get_link_url`` method on the class.
|
||||||
|
|
||||||
|
.. attribute:: allowed_data_types
|
||||||
|
|
||||||
|
A list that contains the allowed data types of the action. If the
|
||||||
|
datum's type is in this list, the action will be shown on the row
|
||||||
|
for the datum.
|
||||||
|
|
||||||
|
Defaults to be an empty list (``[]``). When set to empty, the action
|
||||||
|
will accept any kind of data.
|
||||||
"""
|
"""
|
||||||
method = "GET"
|
method = "GET"
|
||||||
bound_url = None
|
bound_url = None
|
||||||
|
|
||||||
def __init__(self, verbose_name=None, url=None, attrs=None):
|
def __init__(self, verbose_name=None, allowed_data_types=[],
|
||||||
|
url=None, attrs=None):
|
||||||
super(LinkAction, self).__init__()
|
super(LinkAction, self).__init__()
|
||||||
self.verbose_name = verbose_name or getattr(self,
|
self.verbose_name = verbose_name or getattr(self,
|
||||||
"verbose_name",
|
"verbose_name",
|
||||||
self.name.title())
|
self.name.title())
|
||||||
self.url = getattr(self, "url", url)
|
self.url = getattr(self, "url", url)
|
||||||
if not self.verbose_name:
|
if not self.verbose_name:
|
||||||
raise NotImplementedError('A LinkAction object must have a '
|
raise NotImplementedError('A LinkAction object must have a '
|
||||||
'verbose_name attribute.')
|
'verbose_name attribute.')
|
||||||
|
self.allowed_data_types = getattr(self, "allowed_data_types",
|
||||||
|
allowed_data_types)
|
||||||
if attrs:
|
if attrs:
|
||||||
self.attrs.update(attrs)
|
self.attrs.update(attrs)
|
||||||
|
|
||||||
@ -316,14 +355,37 @@ class FilterAction(BaseAction):
|
|||||||
classes += ("btn-search",)
|
classes += ("btn-search",)
|
||||||
return classes
|
return classes
|
||||||
|
|
||||||
|
def assign_type_string(self, table, data, type_string):
|
||||||
|
for datum in data:
|
||||||
|
setattr(datum, table._meta.data_type_name,
|
||||||
|
type_string)
|
||||||
|
|
||||||
|
def data_type_filter(self, table, data, filter_string):
|
||||||
|
filtered_data = []
|
||||||
|
for data_type in table._meta.data_types:
|
||||||
|
func_name = "filter_%s_data" % data_type
|
||||||
|
filter_func = getattr(self, func_name, None)
|
||||||
|
if not filter_func and not callable(filter_func):
|
||||||
|
# The check of filter function implementation should happen
|
||||||
|
# in the __init__. However, the current workflow of DataTable
|
||||||
|
# and actions won't allow it. Need to be fixed in the future.
|
||||||
|
cls_name = self.__class__.__name__
|
||||||
|
raise NotImplementedError("You must define a %s method "
|
||||||
|
"for %s data type in %s." %
|
||||||
|
(func_name, data_type, cls_name))
|
||||||
|
_data = filter_func(table, data, filter_string)
|
||||||
|
self.assign_type_string(table, _data, data_type)
|
||||||
|
filtered_data.extend(_data)
|
||||||
|
return filtered_data
|
||||||
|
|
||||||
def filter(self, table, data, filter_string):
|
def filter(self, table, data, filter_string):
|
||||||
""" Provides the actual filtering logic.
|
""" Provides the actual filtering logic.
|
||||||
|
|
||||||
This method must be overridden by subclasses and return
|
This method must be overridden by subclasses and return
|
||||||
the filtered data.
|
the filtered data.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("The filter method has not been implemented "
|
raise NotImplementedError("The filter method has not been "
|
||||||
"by %s." % self.__class__)
|
"implemented by %s." % self.__class__)
|
||||||
|
|
||||||
|
|
||||||
class BatchAction(Action):
|
class BatchAction(Action):
|
||||||
|
@ -78,6 +78,14 @@ class Column(html.HTMLElement):
|
|||||||
A string or callable which returns a URL which will be wrapped around
|
A string or callable which returns a URL which will be wrapped around
|
||||||
this column's text as a link.
|
this column's text as a link.
|
||||||
|
|
||||||
|
.. attribute:: allowed_data_types
|
||||||
|
|
||||||
|
A list of data types for which the link should be created.
|
||||||
|
Default is an empty list (``[]``).
|
||||||
|
|
||||||
|
When the list is empty and the ``link`` attribute is not None, all the
|
||||||
|
rows under this column will be links.
|
||||||
|
|
||||||
.. attribute:: status
|
.. attribute:: status
|
||||||
|
|
||||||
Boolean designating whether or not this column represents a status
|
Boolean designating whether or not this column represents a status
|
||||||
@ -179,10 +187,10 @@ class Column(html.HTMLElement):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, transform, verbose_name=None, sortable=True,
|
def __init__(self, transform, verbose_name=None, sortable=True,
|
||||||
link=None, hidden=False, attrs=None, status=False,
|
link=None, allowed_data_types=[], hidden=False, attrs=None,
|
||||||
status_choices=None, display_choices=None, empty_value=None,
|
status=False, status_choices=None, display_choices=None,
|
||||||
filters=None, classes=None, summation=None, auto=None,
|
empty_value=None, filters=None, classes=None, summation=None,
|
||||||
truncate=None):
|
auto=None, truncate=None):
|
||||||
self.classes = list(classes or getattr(self, "classes", []))
|
self.classes = list(classes or getattr(self, "classes", []))
|
||||||
super(Column, self).__init__()
|
super(Column, self).__init__()
|
||||||
self.attrs.update(attrs or {})
|
self.attrs.update(attrs or {})
|
||||||
@ -204,6 +212,7 @@ class Column(html.HTMLElement):
|
|||||||
self.sortable = sortable
|
self.sortable = sortable
|
||||||
self.verbose_name = verbose_name
|
self.verbose_name = verbose_name
|
||||||
self.link = link
|
self.link = link
|
||||||
|
self.allowed_data_types = allowed_data_types
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
self.status = status
|
self.status = status
|
||||||
self.empty_value = empty_value or '-'
|
self.empty_value = empty_value or '-'
|
||||||
@ -298,11 +307,21 @@ class Column(html.HTMLElement):
|
|||||||
def get_link_url(self, datum):
|
def get_link_url(self, datum):
|
||||||
""" Returns the final value for the column's ``link`` property.
|
""" Returns the final value for the column's ``link`` property.
|
||||||
|
|
||||||
|
If ``allowed_data_types`` of this column is not empty and the datum
|
||||||
|
has an assigned type, check if the datum's type is in the
|
||||||
|
``allowed_data_types`` list. If not, the datum won't be displayed
|
||||||
|
as a link.
|
||||||
|
|
||||||
If ``link`` is a callable, it will be passed the current data object
|
If ``link`` is a callable, it will be passed the current data object
|
||||||
and should return a URL. Otherwise ``get_link_url`` will attempt to
|
and should return a URL. Otherwise ``get_link_url`` will attempt to
|
||||||
call ``reverse`` on ``link`` with the object's id as a parameter.
|
call ``reverse`` on ``link`` with the object's id as a parameter.
|
||||||
Failing that, it will simply return the value of ``link``.
|
Failing that, it will simply return the value of ``link``.
|
||||||
"""
|
"""
|
||||||
|
if self.allowed_data_types:
|
||||||
|
data_type_name = self.table._meta.data_type_name
|
||||||
|
data_type = getattr(datum, data_type_name, None)
|
||||||
|
if data_type and (data_type not in self.allowed_data_types):
|
||||||
|
return None
|
||||||
obj_id = self.table.get_object_id(datum)
|
obj_id = self.table.get_object_id(datum)
|
||||||
if callable(self.link):
|
if callable(self.link):
|
||||||
return self.link(datum)
|
return self.link(datum)
|
||||||
@ -529,12 +548,19 @@ class Cell(html.HTMLElement):
|
|||||||
data = None
|
data = None
|
||||||
exc_info = sys.exc_info()
|
exc_info = sys.exc_info()
|
||||||
raise template.TemplateSyntaxError, exc_info[1], exc_info[2]
|
raise template.TemplateSyntaxError, exc_info[1], exc_info[2]
|
||||||
|
if self.url:
|
||||||
|
# Escape the data inside while allowing our HTML to render
|
||||||
|
data = mark_safe('<a href="%s">%s</a>' % (self.url, escape(data)))
|
||||||
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
if self.column.link:
|
if self.column.link:
|
||||||
url = self.column.get_link_url(self.datum)
|
url = self.column.get_link_url(self.datum)
|
||||||
if url:
|
if url:
|
||||||
# Escape the data inside while allowing our HTML to render
|
return url
|
||||||
data = mark_safe('<a href="%s">%s</a>' % (url, escape(data)))
|
else:
|
||||||
return data
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
@ -565,6 +591,9 @@ class Cell(html.HTMLElement):
|
|||||||
|
|
||||||
def get_default_classes(self):
|
def get_default_classes(self):
|
||||||
""" Returns a flattened string of the cell's CSS classes. """
|
""" Returns a flattened string of the cell's CSS classes. """
|
||||||
|
if not self.url:
|
||||||
|
self.column.classes = [cls for cls in self.column.classes
|
||||||
|
if cls != "anchor"]
|
||||||
column_class_string = self.column.get_final_attrs().get('class', "")
|
column_class_string = self.column.get_final_attrs().get('class', "")
|
||||||
classes = set(column_class_string.split(" "))
|
classes = set(column_class_string.split(" "))
|
||||||
if self.column.status:
|
if self.column.status:
|
||||||
@ -656,6 +685,20 @@ class DataTableOptions(object):
|
|||||||
|
|
||||||
The class which should be used for handling the columns of this table.
|
The class which should be used for handling the columns of this table.
|
||||||
Optional. Default: :class:`~horizon.tables.Column`.
|
Optional. Default: :class:`~horizon.tables.Column`.
|
||||||
|
|
||||||
|
.. attribute:: mixed_data_type
|
||||||
|
|
||||||
|
A toggle to indicate if the table accepts two or more types of data.
|
||||||
|
Optional. Default: :``False``
|
||||||
|
|
||||||
|
.. attribute:: data_types
|
||||||
|
A list of data types that this table would accept. Default to be an
|
||||||
|
empty list, but if the attibute ``mixed_data_type`` is set to ``True``,
|
||||||
|
then this list must have at least one element.
|
||||||
|
|
||||||
|
.. attribute:: data_type_name
|
||||||
|
The name of an attribute to assign to data passed to the table when it
|
||||||
|
accepts mix data. Default: ``"_table_data_type"``
|
||||||
"""
|
"""
|
||||||
def __init__(self, options):
|
def __init__(self, options):
|
||||||
self.name = getattr(options, 'name', self.__class__.__name__)
|
self.name = getattr(options, 'name', self.__class__.__name__)
|
||||||
@ -700,6 +743,25 @@ class DataTableOptions(object):
|
|||||||
# Set runtime table defaults; not configurable.
|
# Set runtime table defaults; not configurable.
|
||||||
self.has_more_data = False
|
self.has_more_data = False
|
||||||
|
|
||||||
|
# Set mixed data type table attr
|
||||||
|
self.mixed_data_type = getattr(options, 'mixed_data_type', False)
|
||||||
|
self.data_types = getattr(options, 'data_types', [])
|
||||||
|
|
||||||
|
# If the data_types has more than 2 elements, set mixed_data_type
|
||||||
|
# to True automatically.
|
||||||
|
if len(self.data_types) > 1:
|
||||||
|
self.mixed_data_type = True
|
||||||
|
|
||||||
|
# However, if the mixed_data_type is set to True manually and the
|
||||||
|
# the data_types is empty, raise an errror.
|
||||||
|
if self.mixed_data_type and len(self.data_types) <= 1:
|
||||||
|
raise ValueError("If mixed_data_type is set to True in class %s, "
|
||||||
|
"data_types should has more than one types" %
|
||||||
|
self.name)
|
||||||
|
|
||||||
|
self.data_type_name = getattr(options, 'data_type_name',
|
||||||
|
"_table_data_type")
|
||||||
|
|
||||||
|
|
||||||
class DataTableMetaclass(type):
|
class DataTableMetaclass(type):
|
||||||
""" Metaclass to add options to DataTable class and collect columns. """
|
""" Metaclass to add options to DataTable class and collect columns. """
|
||||||
@ -842,9 +904,14 @@ class DataTable(object):
|
|||||||
filter_string = self.get_filter_string()
|
filter_string = self.get_filter_string()
|
||||||
request_method = self._meta.request.method
|
request_method = self._meta.request.method
|
||||||
if filter_string and request_method == action.method:
|
if filter_string and request_method == action.method:
|
||||||
self._filtered_data = action.filter(self,
|
if self._meta.mixed_data_type:
|
||||||
self.data,
|
self._filtered_data = action.data_type_filter(self,
|
||||||
filter_string)
|
self.data,
|
||||||
|
filter_string)
|
||||||
|
else:
|
||||||
|
self._filtered_data = action.filter(self,
|
||||||
|
self.data,
|
||||||
|
filter_string)
|
||||||
return self._filtered_data
|
return self._filtered_data
|
||||||
|
|
||||||
def get_filter_string(self):
|
def get_filter_string(self):
|
||||||
@ -862,7 +929,10 @@ class DataTable(object):
|
|||||||
def _filter_action(self, action, request, datum=None):
|
def _filter_action(self, action, request, datum=None):
|
||||||
try:
|
try:
|
||||||
# Catch user errors in permission functions here
|
# Catch user errors in permission functions here
|
||||||
return action._allowed(request, datum)
|
row_matched = True
|
||||||
|
if self._meta.mixed_data_type:
|
||||||
|
row_matched = action.data_type_matched(datum)
|
||||||
|
return action._allowed(request, datum) and row_matched
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception("Error while checking action permissions.")
|
LOG.exception("Error while checking action permissions.")
|
||||||
return None
|
return None
|
||||||
|
@ -154,3 +154,54 @@ class DataTableView(MultiTableView):
|
|||||||
context = super(DataTableView, self).get_context_data(**kwargs)
|
context = super(DataTableView, self).get_context_data(**kwargs)
|
||||||
context[self.context_object_name] = self.table
|
context[self.context_object_name] = self.table
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class MixedDataTableView(DataTableView):
|
||||||
|
""" A class-based generic view to handle DataTable with mixed data
|
||||||
|
types.
|
||||||
|
|
||||||
|
Basic usage is the same as DataTableView.
|
||||||
|
|
||||||
|
Three steps are required to use this view:
|
||||||
|
#. Set the ``table_class`` attribute with desired
|
||||||
|
:class:`~horizon.tables.DataTable` class. In the class the
|
||||||
|
``data_types`` list should have at least two elements.
|
||||||
|
|
||||||
|
#. Define a ``get_{{ data_type }}_data`` method for each data type
|
||||||
|
which returns a set of data for the table.
|
||||||
|
|
||||||
|
#. Specify a template for the ``template_name`` attribute.
|
||||||
|
"""
|
||||||
|
table_class = None
|
||||||
|
context_object_name = 'table'
|
||||||
|
|
||||||
|
def _get_data_dict(self):
|
||||||
|
if not self._data:
|
||||||
|
table = self.table_class
|
||||||
|
self._data = {table._meta.name: []}
|
||||||
|
for data_type in table._meta.data_types:
|
||||||
|
func_name = "get_%s_data" % data_type
|
||||||
|
data_func = getattr(self, func_name, None)
|
||||||
|
if data_func is None:
|
||||||
|
cls_name = self.__class__.__name__
|
||||||
|
raise NotImplementedError("You must define a %s method "
|
||||||
|
"for %s data type in %s." %
|
||||||
|
(func_name, data_type, cls_name))
|
||||||
|
data = data_func()
|
||||||
|
self.assign_type_string(data, data_type)
|
||||||
|
self._data[table._meta.name].extend(data)
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def assign_type_string(self, data, type_string):
|
||||||
|
for datum in data:
|
||||||
|
setattr(datum, self.table_class._meta.data_type_name,
|
||||||
|
type_string)
|
||||||
|
|
||||||
|
def get_table(self):
|
||||||
|
self.table = super(MixedDataTableView, self).get_table()
|
||||||
|
if not self.table._meta.mixed_data_type:
|
||||||
|
raise AttributeError('You must have at least two elements in '
|
||||||
|
'the data_types attibute '
|
||||||
|
'in table %s to use MixedDataTableView.'
|
||||||
|
% self.table._meta.name)
|
||||||
|
return self.table
|
||||||
|
Loading…
Reference in New Issue
Block a user