Added IDs and identifiable classes to all action buttons.

Fixes bug 953483.

Change-Id: Ie6b858a9a595d024f71ca372a11b97a454b3b1e8
This commit is contained in:
Gabriel Hurley 2012-03-13 20:35:23 -07:00
parent ee3e890466
commit cb8f3ddf4e
20 changed files with 148 additions and 68 deletions

View File

@ -41,7 +41,7 @@ How to use Horizon in your own projects.
intro
quickstart
topics/deployment
topics/branding
topics/customizing
Developer Docs
==============

View File

@ -28,8 +28,10 @@ Alternately specify the listen IP and port::
Once the Horizon server is running point a web browser to http://localhost:8000
or to the IP and port the server is listening.
.. note:: The ``DevStack`` project (http://devstack.org/) can be used to install
an OpenStack development environment from scratch.
.. note::
The ``DevStack`` project (http://devstack.org/) can be used to install
an OpenStack development environment from scratch.
.. note::

View File

@ -1,29 +0,0 @@
==============================
Change the branding of Horizon
==============================
Changing the Page Title
=======================
The OpenStack Dashboard Page Title branding (i.e. "**OpenStack** Dashboard")
can be overwritten by adding the attribute ``SITE_BRANDING``
to ``local_settings.py`` with the value being the desired name.
The file ``local_settings.py`` can be found at the Horizon directory path of
``horizon/openstack-dashboard/local/local_settings.py``.
Changing the Page Logo
=======================
The OpenStack Logo is pulled in through ``style.css``::
#splash .modal {
background: #fff url(../images/logo.png) no-repeat center 35px;
h1.brand a {
background: url(../images/logo.png) top left no-repeat;
To override the OpenStack Logo image, replace the image at the directory path
``horizon/openstack-dashboard/dashboard/static/dashboard/images/logo.png``.
The dimensions should be ``width: 108px, height: 121px``.

View File

@ -0,0 +1,72 @@
===================
Customizing Horizon
===================
Changing the Site Title
=======================
The OpenStack Dashboard Site Title branding (i.e. "**OpenStack** Dashboard")
can be overwritten by adding the attribute ``SITE_BRANDING``
to ``local_settings.py`` with the value being the desired name.
The file ``local_settings.py`` can be found at the Horizon directory path of
``horizon/openstack-dashboard/local/local_settings.py``.
Changing the Logo
=================
The OpenStack Logo is pulled in through ``style.css``::
#splash .modal {
background: #fff url(../images/logo.png) no-repeat center 35px;
h1.brand a {
background: url(../images/logo.png) top left no-repeat;
To override the OpenStack Logo image, replace the image at the directory path
``horizon/openstack-dashboard/dashboard/static/dashboard/images/logo.png``.
The dimensions should be ``width: 108px, height: 121px``.
Button Icons
============
Horizon provides hooks for customizing the look and feel of each class of
button on the site. The following classes are used to identify each type of
button:
* Generic Classes
* btn-search
* btn-delete
* btn-upload
* btn-download
* btn-create
* btn-edit
* btn-list
* btn-copy
* btn-camera
* btn-stats
* btn-enable
* btn-disable
* Floating IP-specific Classes
* btn-allocate
* btn-release
* btn-associate
* btn-disassociate
* Instance-specific Classes
* btn-launch
* btn-terminate
* btn-reboot
* btn-pause
* btn-suspend
* btn-console
* btn-log
* Volume-specific classes
* btn-detach
Additionally, the site-wide default button classes can be configured by
setting ``ACTION_CSS_CLASSES`` to a tuple of the classes you wish to appear
on all action buttons in your ``local_settings.py`` file.

View File

@ -32,7 +32,7 @@ LOG = logging.getLogger(__name__)
class AllocateIP(tables.LinkAction):
name = "allocate"
verbose_name = _("Allocate IP To Project")
classes = ("ajax-modal",)
classes = ("ajax-modal", "btn-allocate")
url = "horizon:nova:access_and_security:floating_ips:allocate"
def single(self, data_table, request, *args):
@ -45,7 +45,7 @@ class ReleaseIPs(tables.BatchAction):
action_past = _("Released")
data_type_singular = _("Floating IP")
data_type_plural = _("Floating IPs")
classes = ('btn-danger',)
classes = ('btn-danger', 'btn-release')
def action(self, request, obj_id):
api.tenant_floating_ip_release(request, obj_id)
@ -55,7 +55,7 @@ class AssociateIP(tables.LinkAction):
name = "associate"
verbose_name = _("Associate IP")
url = "horizon:nova:access_and_security:floating_ips:associate"
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-associate")
def allowed(self, request, fip):
if fip.instance_id:
@ -66,6 +66,7 @@ class AssociateIP(tables.LinkAction):
class DisassociateIP(tables.Action):
name = "disassociate"
verbose_name = _("Disassociate IP")
classes = ("btn-disassociate", "btn-danger")
def allowed(self, request, fip):
if fip.instance_id:

View File

@ -37,14 +37,14 @@ class ImportKeyPair(tables.LinkAction):
name = "import"
verbose_name = _("Import Keypair")
url = "horizon:nova:access_and_security:keypairs:import"
attrs = {"class": "ajax-modal btn"}
classes = ("ajax-modal", "btn-upload")
class CreateKeyPair(tables.LinkAction):
name = "create"
verbose_name = _("Create Keypair")
url = "horizon:nova:access_and_security:keypairs:create"
attrs = {"class": "ajax-modal btn"}
classes = ("ajax-modal", "btn-create")
class KeypairsTable(tables.DataTable):

View File

@ -43,14 +43,14 @@ class CreateGroup(tables.LinkAction):
name = "create"
verbose_name = _("Create Security Group")
url = "horizon:nova:access_and_security:security_groups:create"
classes = ("ajax-modal",)
classes = ("ajax-modal", "btn-create")
class EditRules(tables.LinkAction):
name = "edit_rules"
verbose_name = _("Edit Rules")
url = "horizon:nova:access_and_security:security_groups:edit_rules"
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-edit")
class SecurityGroupsTable(tables.DataTable):

View File

@ -25,7 +25,6 @@ from django.utils import http
from django.utils.translation import ugettext as _
from horizon import api
from horizon import exceptions
from horizon import tables
@ -63,20 +62,21 @@ class CreateContainer(tables.LinkAction):
name = "create"
verbose_name = _("Create Container")
url = "horizon:nova:containers:create"
classes = ("ajax-modal",)
classes = ("ajax-modal", "btn-create")
class ListObjects(tables.LinkAction):
name = "list_objects"
verbose_name = _("List Objects")
url = "horizon:nova:containers:object_index"
classes = ("btn-list",)
class UploadObject(tables.LinkAction):
name = "upload"
verbose_name = _("Upload Object")
url = "horizon:nova:containers:object_upload"
classes = ("ajax-modal",)
classes = ("ajax-modal", "btn-upload")
def get_link_url(self, datum=None):
# Usable for both the container and object tables
@ -130,7 +130,7 @@ class CopyObject(tables.LinkAction):
name = "copy"
verbose_name = _("Copy")
url = "horizon:nova:containers:object_copy"
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-copy")
def get_link_url(self, obj):
return reverse(self.url, args=(http.urlquote(obj.container.name),
@ -141,6 +141,7 @@ class DownloadObject(tables.LinkAction):
name = "download"
verbose_name = _("Download")
url = "horizon:nova:containers:object_download"
classes = ("btn-download",)
def get_link_url(self, obj):
#assert False, obj.__dict__['_apiresource'].__dict__

View File

@ -43,14 +43,14 @@ class LaunchImage(tables.LinkAction):
name = "launch"
verbose_name = _("Launch")
url = "horizon:nova:images_and_snapshots:images:launch"
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-launch")
class EditImage(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
url = "horizon:nova:images_and_snapshots:images:update"
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-edit")
def get_image_type(image):

View File

@ -29,7 +29,6 @@ LOG = logging.getLogger(__name__)
class DeleteVolumeSnapshot(tables.DeleteAction):
data_type_singular = _("Volume Snapshot")
data_type_plural = _("Volume Snapshots")
classes = ('btn-danger',)
def delete(self, request, obj_id):
api.volume_snapshot_delete(request, obj_id)

View File

@ -55,7 +55,7 @@ class TerminateInstance(tables.BatchAction):
action_past = _("Terminated")
data_type_singular = _("Instance")
data_type_plural = _("Instances")
classes = ('btn-danger',)
classes = ('btn-danger', 'btn-terminate')
def action(self, request, obj_id):
api.server_delete(request, obj_id)
@ -67,7 +67,7 @@ class RebootInstance(tables.BatchAction):
action_past = _("Rebooted")
data_type_singular = _("Instance")
data_type_plural = _("Instances")
classes = ('btn-danger',)
classes = ('btn-danger', 'btn-reboot')
def allowed(self, request, instance=None):
return instance.status in ACTIVE_STATES or instance.status == 'SHUTOFF'
@ -82,6 +82,7 @@ class TogglePause(tables.BatchAction):
action_past = (_("Paused"), _("Unpaused"))
data_type_singular = _("Instance")
data_type_plural = _("Instances")
classes = ("btn-pause")
def allowed(self, request, instance=None):
self.paused = False
@ -107,6 +108,7 @@ class ToggleSuspend(tables.BatchAction):
action_past = (_("Suspended"), _("Resumed"))
data_type_singular = _("Instance")
data_type_plural = _("Instances")
classes = ("btn-suspend")
def allowed(self, request, instance=None):
self.suspended = False
@ -130,20 +132,21 @@ class LaunchLink(tables.LinkAction):
name = "launch"
verbose_name = _("Launch Instance")
url = "horizon:nova:images_and_snapshots:index"
classes = ("btn-launch",)
class EditInstance(tables.LinkAction):
name = "edit"
verbose_name = _("Edit Instance")
url = "horizon:nova:instances_and_volumes:instances:update"
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-edit")
class SnapshotLink(tables.LinkAction):
name = "snapshot"
verbose_name = _("Snapshot")
url = "horizon:nova:images_and_snapshots:snapshots:create"
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-camera")
def allowed(self, request, instance=None):
return instance.status in ACTIVE_STATES
@ -153,6 +156,7 @@ class ConsoleLink(tables.LinkAction):
name = "console"
verbose_name = _("VNC Console")
url = "horizon:nova:instances_and_volumes:instances:vnc"
classes = ("btn-console",)
def allowed(self, request, instance=None):
return instance.status in ACTIVE_STATES
@ -162,6 +166,7 @@ class LogLink(tables.LinkAction):
name = "log"
verbose_name = _("View Log")
url = "horizon:nova:instances_and_volumes:instances:console"
classes = ("btn-log",)
def allowed(self, request, instance=None):
return instance.status in ACTIVE_STATES

View File

@ -34,7 +34,6 @@ DELETABLE_STATES = ("available", "error")
class DeleteVolume(tables.DeleteAction):
data_type_singular = _("Volume")
data_type_plural = _("Volumes")
classes = ('btn-danger',)
def delete(self, request, obj_id):
api.volume_delete(request, obj_id)
@ -58,14 +57,14 @@ class CreateVolume(tables.LinkAction):
name = "create"
verbose_name = _("Create Volume")
url = "%s:volumes:create" % URL_PREFIX
classes = ("ajax-modal",)
classes = ("ajax-modal", "btn-create")
class EditAttachments(tables.LinkAction):
name = "attachments"
verbose_name = _("Edit Attachments")
url = "%s:volumes:attach" % URL_PREFIX
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-edit")
def allowed(self, request, volume=None):
return volume.status in ("available", "in-use")
@ -75,7 +74,7 @@ class CreateSnapshot(tables.LinkAction):
name = "snapshots"
verbose_name = _("Create Snapshot")
url = "%s:volumes:create_snapshot" % URL_PREFIX
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-camera")
def allowed(self, request, volume=None):
return volume.status == "available"
@ -154,7 +153,7 @@ class DetachVolume(tables.BatchAction):
action_past = _("Detached")
data_type_singular = _("Volume")
data_type_plural = _("Volumes")
classes = ('btn-danger',)
classes = ('btn-danger', 'btn-detach')
def action(self, request, obj_id):
instance_id = self.table.get_object_by_id(obj_id)['serverId']

View File

@ -21,7 +21,7 @@ class CreateFlavor(tables.LinkAction):
name = "create"
verbose_name = _("Create Flavor")
url = "horizon:syspanel:flavors:create"
classes = ("ajax-modal",)
classes = ("ajax-modal", "btn-create")
class FlavorsTable(tables.DataTable):

View File

@ -16,26 +16,28 @@ class ModifyQuotasLink(tables.LinkAction):
name = "quotas"
verbose_name = _("Modify Quotas")
url = "horizon:syspanel:projects:quotas"
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-edit")
class ViewMembersLink(tables.LinkAction):
name = "users"
verbose_name = _("Modify Users")
url = "horizon:syspanel:projects:users"
classes = ("btn-download",)
class UsageLink(tables.LinkAction):
name = "usage"
verbose_name = _("View Usage")
url = "horizon:syspanel:projects:usage"
classes = ("btn-stats",)
class EditLink(tables.LinkAction):
name = "update"
verbose_name = _("Edit Project")
url = "horizon:syspanel:projects:update"
attrs = {"class": "ajax-modal"}
classes = ("ajax-modal", "btn-edit")
class CreateLink(tables.LinkAction):

View File

@ -1,10 +1,7 @@
import logging
from django import shortcuts
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _
from horizon import api
from horizon import tables

View File

@ -15,20 +15,21 @@ class CreateUserLink(tables.LinkAction):
name = "create"
verbose_name = _("Create User")
url = "horizon:syspanel:users:create"
classes = ("ajax-modal",)
classes = ("ajax-modal", "btn-create")
class EditUserLink(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
url = "horizon:syspanel:users:update"
classes = ("ajax-modal",)
classes = ("ajax-modal", "btn-edit")
class EnableUsersAction(tables.Action):
name = "enable"
verbose_name = _("Enable")
verbose_name_plural = _("Enable Users")
classes = ("btn-enable",)
def allowed(self, request, user):
return not user.enabled
@ -57,6 +58,7 @@ class DisableUsersAction(tables.Action):
name = "disable"
verbose_name = _("Disable")
verbose_name_plural = _("Disable Users")
classes = ("btn-disable",)
def allowed(self, request, user):
return user.enabled

View File

@ -32,6 +32,7 @@ LOG = logging.getLogger(__name__)
# For Bootstrap integration; can be overridden in settings.
ACTION_CSS_CLASSES = ("btn", "btn-small")
STRING_SEPARATOR = "__"
class BaseAction(html.HTMLElement):
@ -41,6 +42,10 @@ class BaseAction(html.HTMLElement):
requires_input = False
preempt = False
def __init__(self):
super(BaseAction, self).__init__()
self.id_counter = 0
def allowed(self, request, datum):
""" Determine whether this action is allowed for the current request.
@ -64,11 +69,21 @@ class BaseAction(html.HTMLElement):
def get_default_classes(self):
"""
Returns a list of the default classes for the tab. Defaults to
Returns a list of the default classes for the action. Defaults to
``["btn", "btn-small"]``.
"""
return getattr(settings, "ACTION_CSS_CLASSES", ACTION_CSS_CLASSES)
def get_default_attrs(self):
"""
Returns a list of the default HTML attributes for the action. Defaults
to returning an ``id`` attribute with the value
``{{ table.name }}__action_{{ action.name }}__{{ creation counter }}``.
"""
bits = (self.table.name, "action_%s" % self.name, str(self.id_counter))
self.id_counter += 1
return {"id": STRING_SEPARATOR.join(bits)}
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self.name)
@ -286,6 +301,11 @@ class FilterAction(BaseAction):
"""
return "__".join([self.table.name, self.name, self.param_name])
def get_default_classes(self):
classes = super(FilterAction, self).get_default_classes()
classes += ("btn-search",)
return classes
def filter(self, table, data, filter_string):
""" Provides the actual filtering logic.
@ -452,10 +472,14 @@ class DeleteAction(BatchAction):
name = "delete"
action_present = _("Delete")
action_past = _("Deleted")
classes = ('btn-danger',)
def action(self, request, obj_id):
return self.delete(request, obj_id)
def delete(self, request, obj_id):
raise NotImplementedError("DeleteAction must define a delete method.")
def get_default_classes(self):
classes = super(DeleteAction, self).get_default_classes()
classes += ("btn-danger", "btn-delete")
return classes

View File

@ -2,13 +2,13 @@
{% 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" class="btn btn-small filter">Filter</button>
<button type="submit" {{ filter.attr_string|safe }}>Filter</button>
</div>
{% endif %}
{% for action in table_actions %}
{% if action != filter %}
{% if action.method != "GET" %}
<button class='btn btn-small {{ action.classes|join:" " }}' name="action" value="{{ action.get_param_name }}" type="submit">{% if action.handles_multiple %}{{ action.verbose_name_plural }}{% else %}{{ action.verbose_name }}{% endif %}</button>
<button {{ action.attr_string|safe }} name="action" value="{{ action.get_param_name }}" type="submit">{% if action.handles_multiple %}{{ action.verbose_name_plural }}{% else %}{{ action.verbose_name }}{% endif %}</button>
{% else %}
<a href='{{ action.get_link_url }}' {{ action.attr_string|safe }}>{{ action.verbose_name }}</a>
{% endif %}

View File

@ -8,6 +8,7 @@ from horizon.templatetags.sizeformat import mbformat
class CSVSummary(tables.LinkAction):
name = "csv_summary"
verbose_name = _("Download CSV Summary")
classes = ("btn-download",)
def get_link_url(self, usage=None):
return self.table.kwargs['usage'].csv_link()

View File

@ -16,13 +16,17 @@ class HTMLElement(object):
"""
return []
def get_default_attrs(self):
return {}
@property
def attr_string(self):
"""
Returns a flattened string of HTML attributes based on the
``attrs`` dict provided to the class.
"""
final_attrs = copy.copy(self.attrs)
final_attrs = copy.copy(self.get_default_attrs())
final_attrs.update(self.attrs)
# Handle css class concatenation
default = " ".join(self.get_default_classes())
defined = self.attrs.get('class', '')