horizon/horizon/horizon/tables/actions.py

275 lines
9.7 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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
import new
from django.forms.util import flatatt
from django.core import urlresolvers
LOG = logging.getLogger(__name__)
class BaseAction(object):
""" Common base class for all ``Action`` classes. """
table = None
handles_multiple = False
attrs = {}
name = None
requires_input = False
def allowed(self, request, datum):
""" Determine whether this action is allowed for the current request.
This method is meant to be overridden with more specific checks.
"""
return True
def update(self, request, datum):
""" Allows per-action customization based on current conditions.
This is particularly useful when you wish to create a "toggle"
action that will be rendered differently based on the value of an
attribute on the current row's data.
By default this method is a no-op.
"""
pass
@property
def attr_string(self):
"""
Returns a flattened string of HTML attributes based on the
``attrs`` dict provided to the class.
"""
return flatatt(self.attrs)
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self.name)
class Action(BaseAction):
""" Represents an action which can be taken on this table's data.
.. attribute:: name
The short name or "slug" representing this action. Defaults to the
name of the ``Action`` class.
.. attribute:: verbose_name
A descriptive name used for display purposes. Defaults to the
value of ``name`` with the first letter of each word capitalized.
.. attribute:: verbose_name_plural
Used like ``verbose_name`` in cases where ``handles_multiple`` is
``True``. Defaults to ``verbose_name`` with the letter "s" appended.
.. attribute:: method
The HTTP method for this action. Defaults to ``POST``. Other methods
may or may not succeed currently.
.. attribute:: requires_input
Boolean value indicating whether or not this action can be taken
without any additional input (e.g. an object id). Defaults to ``True``.
At least one of the following methods must be defined:
.. method:: single(self, data_table, request, object_id)
Handler for a single-object action.
.. method:: multiple(self, data_table, request, object_ids)
Handler for multi-object actions.
.. method:: handle(self, data_table, request, object_ids)
If a single function can work for both single-object and
multi-object cases then simply providing a ``handle`` function
will internally route both ``single`` and ``multiple`` requests
to ``handle`` with the calls from ``single`` being transformed
into a list containing only the single object id.
"""
method = "POST"
requires_input = True
def __init__(self, verbose_name=None, verbose_name_plural=None,
single_func=None, multiple_func=None, handle_func=None,
handles_multiple=False, attrs=None, requires_input=True):
super(Action, self).__init__()
self.name = unicode(getattr(self, 'name', self.__class__.__name__))
verbose_name = verbose_name or self.name.title()
self.verbose_name = unicode(getattr(self,
"verbose_name",
verbose_name))
verbose_name_plural = verbose_name_plural or "%ss" % self.verbose_name
self.verbose_name_plural = unicode(getattr(self,
"verbose_name_plural",
verbose_name_plural))
self.handles_multiple = getattr(self,
"handles_multiple",
handles_multiple)
self.requires_input = getattr(self,
"requires_input",
requires_input)
if attrs:
self.attrs.update(attrs)
# Don't set these if they're None
if single_func:
self.single = single_func
if multiple_func:
self.multiple = multiple_func
if handle_func:
self.handle = handle_func
# Ensure we have the appropriate methods
has_handler = hasattr(self, 'handle') and callable(self.handle)
has_single = hasattr(self, 'single') and callable(self.single)
has_multiple = hasattr(self, 'multiple') and callable(self.multiple)
if has_handler or has_multiple:
self.handles_multiple = True
if not has_handler and (not has_single or has_multiple):
raise ValueError('You must define either a "handle" method '
' or a "single" or "multiple" method.')
if not has_single:
def single(self, data_table, request, object_id):
return self.handle(data_table, request, [object_id])
self.single = new.instancemethod(single, self)
if not has_multiple and self.handles_multiple:
def multiple(self, data_table, request, object_ids):
return self.handle(data_table, request, object_ids)
self.multiple = new.instancemethod(multiple, self)
def get_param_name(self):
""" Returns the full POST parameter name for this action.
Defaults to
``{{ table.name }}__{{ action.name }}``.
"""
return "__".join([self.table.name, self.name])
class LinkAction(BaseAction):
""" A table action which is simply a link rather than a form POST.
.. attribute:: verbose_name
A string which will be rendered as the link text. (Required)
.. attribute:: url
A string or a callable which resolves to a url to be used as the link
target. (Required)
"""
method = "GET"
bound_url = None
def __init__(self, name=None, verbose_name=None, url=None, attrs=None):
super(LinkAction, self).__init__()
self.name = name or unicode(getattr(self,
"name",
self.__class__.__name__))
verbose_name = verbose_name or self.name.title()
self.verbose_name = unicode(getattr(self,
"verbose_name",
verbose_name))
self.url = getattr(self, "url", url)
if not self.verbose_name:
raise ValueError('A LinkAction object must have a '
'verbose_name attribute.')
if not self.url:
raise ValueError('A LinkAction object must have a '
'url attribute.')
if attrs:
self.attrs.update(attrs)
def get_link_url(self, datum=None, *args, **kwargs):
""" Returns the final URL based on the value of ``url``.
If ``url`` is callable it will call the function.
If not, it will then try to call ``reverse`` on ``url``.
Failing that, it will simply return the value of ``url`` as-is.
When called for a row action, the current row data object will be
passed as the first parameter.
"""
if callable(self.url):
return self.url(datum, *args, **kwargs)
try:
if datum:
obj_id = self.table.get_object_id(datum)
return urlresolvers.reverse(self.url, args=(obj_id,))
else:
return urlresolvers.reverse(self.url)
except urlresolvers.NoReverseMatch, ex:
LOG.info('No reverse found for "%s": %s' % (self.url, ex))
return self.url
class FilterAction(BaseAction):
""" A base class representing a filter action for a table.
.. attribute:: name
The short name or "slug" representing this action. Defaults to
``"filter"``.
.. attribute:: verbose_name
A descriptive name used for display purposes. Defaults to the
value of ``name`` with the first letter of each word capitalized.
.. attribute:: param_name
A string representing the name of the request parameter used for the
search term. Default: ``"q"``.
"""
method = "GET"
name = "filter"
def __init__(self, name=None, verbose_name=None, param_name=None):
super(FilterAction, self).__init__()
self.name = name or self.name
self.verbose_name = unicode(verbose_name) or self.name
self.param_name = param_name or 'q'
def get_param_name(self):
""" Returns the full query parameter name for this action.
Defaults to
``{{ table.name }}__{{ action.name }}__{{ action.param_name }}``.
"""
return "__".join([self.table.name, self.name, self.param_name])
def filter(self, table, data, filter_string):
""" Provides the actual filtering logic.
This method must be overridden by subclasses and return
the filtered data.
"""
raise NotImplementedError("The filter method has not been implemented "
"by %s." % self.__class__)