Developer's Guide

  • Docs Home
  • Community Home

6. Other Customizations

6.1. Adding Tabs

This section will show how to add a new tab in Zenoss or modify existing one by means of ZenPack or zendmd.

A tab in Zenoss is an object property that resides within the following structure:

    factory_type_information = (
        {
            'immediate_view' : 'deviceOrganizerStatus',
            'actions'        :
            (
                { 'id'            : 'status'
                , 'name'          : 'Status'
                , 'action'        : 'deviceOrganizerStatus'
          , 'permissions'   : (permissions.view,)
                },
            )
        },
    )

For example, tabs in the Locations screen are created from the Python class definition

Location(DeviceOrganizer, ZenPackable)

which resides in the module Location.py in the $ZENPATH/Products/ZenModel directory.

Zenoss works with class instances which are created runtime by Zope. These objects are packed within database which is called ZODB. If you want to modify some object properties you should connect to ZODB and get the object first, modify it and save your changes.

The following example shows the procedure for adding a new tab to Locations screen. This code is executed from __init__.py of an example ZenPack.

import Globals
import transaction
import os.path

skinsDir = os.path.join(os.path.dirname(__file__), 'skins')
from Products.CMFCore.DirectoryView import registerDirectory
if os.path.isdir(skinsDir):
    registerDirectory(skinsDir, globals())

from AccessControl import Permissions as permissions
from Products.ZenModel.ZenPack import ZenPackBase
from Products.ZenUtils.Utils import zenPath
from Products.ZenModel.ZenossSecurity import *
from Products.ZenUtils.ZenScriptBase import ZenScriptBase

class ZenPack(ZenPackBase):
    olMapTab = { 'id'            : 'olgeomaptab'
                , 'name'          : 'OpenLayers Map'
                , 'action'        : 'OLGeoMapTab'
                , 'permissions'   : (permissions.view,)
                }

    def _registerOLMapTab(self, app):
        # Register new tab in locations
        dmdloc = self.getDmdRoot('Locations')
        finfo = dmdloc.factory_type_information
        actions = list(finfo[0]['actions'])
        for i in range(len(actions)):
            if(self.olMapTab['id'] in actions[i].values()):
                return
        actions.append(self.olMapTab)
        finfo[0]['actions'] = tuple(actions)
        dmdloc.factory_type_information = finfo
        transaction.commit()

    def _unregisterOLMapTab(self, app):
        dmdloc = self.getDmdRoot('Locations')
        finfo = dmdloc.factory_type_information
        actions = list(finfo[0]['actions'])
        for i in range(len(actions)):
            if(self.olMapTab['id'] in actions[i].values()):
                actions.remove(actions[i])
        finfo[0]['actions'] = tuple(actions)
        dmdloc.factory_type_information = finfo
        transaction.commit()

    def install(self, app):
        ZenPackBase.install(self, app)
        self._registerOLMapTab(app)

    def upgrade(self, app):
        ZenPackBase.upgrade(self, app)
        self._registerOLMapTab(app)

    def remove(self, app, junk):
        ZenPackBase.remove(self, app, junk)
        zpm = app.zport.ZenPortletManager
        self._unregisterOLMapTab(app)

The class method _registerOLMapTab(self, app) registers the modified property of object Locations , which resides in /zport/dmd/Locations in the ZODB.

The function getDmdRoot('Locations') returns the class instance of class Location which is in ZopeDB. Next we get the dictionary of its factory_type_information property. Modify this, so that a new dictionary defining the tab is appended to it. The tab structure is defined in olMapTab dictionary. The id field is the identification name of this tab. You can put any string here. The name field is the string that is shown on your new tab, action points to the template that is executed when you click on the tab and should be accessible in Zope. The permissions field is default permissions for zenoss user to execute the template this tab points to. This line dmdloc.factory_type_information = finfo is very important because Zope won't detect any change to the persistent object and transaction.commit() won't save any modifications to the object. The rule here is that commit() saves only modifications of object that executes its setattr() method.

Of course every step shown above can be done manually within the zendmd prompt. The following session shows adding new tab to Locations in zendmd:

zenoss@db-server:/home/geonick$ zendmd
Welcome to zenoss dmd command shell!
use zhelp() to list commands
>>> from AccessControl import Permissions as permissions
>>> locobj = dmd.getDmdRoot('Locations')
>>> locobj
<Location at /zport/dmd/Locations>
>>> finfo = locobj.factory_type_information
>>> finfo
({'immediate_view': 'deviceOrganizerStatus', 
'actions': ({'action': 'deviceOrganizerStatus',
'id': 'status', 'name': 'Status', 'permissions': ('View',)}, 
{'action': 'viewEvents', 'id': 'events',
'name': 'Events', 'permissions': ('View',)}, 
{'action': 'deviceOrganizerManage', 'id': 'manage', 'name'
: 'Administration', 'permissions': ('Manage DMD',)}, 
{'action': 'locationGeoMap', 'id': 'geomap', 'name
: 'Map', 'permissions': ('View',)})},)
>>> actions = list(finfo[0]['actions'])
>>> olMapTab = {'id': 'olgeomaptab', 'name': 'OpenLayers Map', 
'action': 'OLGeoMapTab','permissions': (permissions.view,)}
>>> for i in range(len(actions)):
...     if(olMapTab['id'] in actions[i].values()):
...             break
...
>>> actions.append(olMapTab)
>>> finfo[0]['actions'] = tuple(actions)
>>> locobj.factory_type_information = finfo
>>> locobj.factory_type_information
({'immediate_view': 'deviceOrganizerStatus', 'actions': (
{'action': 'deviceOrganizerStatus',
'id': 'status', 'name': 'Status', 'permissions': ('View',)}, 
{'action': 'viewEvents',
'id': 'events', 'name': 'Events', 'permissions': ('View',)}, 
{'action': 'deviceOrganizerManage',
'id': 'manage', 'name': 'Administration', 'permissions': ('Manage DMD',)}, 
{'action': 'locationGeoMap',
'id': 'geomap', 'name': 'Map', 'permissions': ('View',)}, 
{'action': 'OLGeoMapTab', 'permissions': ('View',),
'id': 'olgeomaptab', 'name': 'OpenLayers Map'})},)
>>>commit()

After commit() the new tab should be in Locations. Don't forget to provide the template file.

Submitted by Nikolai Georgiev

6.2. Adding a Dialog

The dialog container exists on every page in Zenoss; it's a DIV element with the id attribute of dialog. Loading a dialog performs two actions:

  1. Fetching (via an XHR) HTML to display inside the dialog container

  2. Showing the dialog container. These can be accomplished by calling the show() method on the dialog container, passing the event and an URL that will return the contents:

        $('dialog').show(this.event, 'dialog_MyDialog')

    The dialog can then be hidden with, predictably, $('dialog').hide(). Since dialogs are almost always loaded via clicking on a menu item, menu items whose isdialog attribute is True will generate the JavaScript to show the dialog automatically. See the Section 6.3, “Adding a New Menu or Menu Item” section of this guide for more information.'

As for the dialog box contents themselves, any valid HTML will do, but certain conventions exist. Dialogs should have a header:

      <h2>Perform Action</h2>

Dialogs should also provide a cancel button:

    <input id="dialog_cancel" type="button" value="Cancel"
           onclick="$('dialog').hide()"/>

The main wrinkle with dialogs occurs in the area of form submission. Some dialogs are self-contained, and can carry their own form that is created and submitted just like any other form. Other dialogs, however, submit forms that exist elsewhere on the page -- for example, dialogs that perform actions against multiple rows checked in a table. These dialogs may use the submit_form method on the dialog container, which submits the form surrounding the menu item that caused the dialog to be loaded to the url passed in to the method. Thus for a table surrounded by a <form> and containing several checkboxes, dialogs loaded by menu items in the table's menu may submit the table's form to a url by providing a button:

    <input type="submit" name="doAction:method" value="Do It"
        tal:attributes="onclick string:
            $('dialog').submit_form('${here/absolute_url_path}')"/>

See the section on Section 1.3, “Zope 2, ZPT and TAL” for more information about tal:attributes and the ${here/absolute_url_path} syntax.

Finally, dialogs that create objects should validate the desired id before submitting. A method on the dialog container called submit_form_and_check(), which accepts the same parameters as submit_form() (URL), will do this. It requires:

  1. A text box with the id 'new_id', the value of which will be checked

  2. A hidden input field with the id checkValidIdPath, with a value containing the path in which the id should be valid (for example, creating a device under /zport/dmd/Devices will require checking that no other devices in /zport/dmd/Devices has the same id, so the value of checkValidIdPath should be "/zport/dmd/Devices". here/getPrimaryUrlPath works well for most cases).

  3. An element with the id errmsg into which the error message from the validation method, if any, will be put

For example, a generic object creation dialog:

    <h2>Create Object</h2>
    <span id="errmsg" style="color:red;"></span>
    <br/>
    <span>ID: </span>
    <input id="new_id" name="id"/>
    <input type="hidden" id="checkValidIdPath"
           tal:attributes="value here/getPrimaryUrlPath"/>
    <br/>
    <input tal:attributes="onclick string:
        return $$('dialog').submit_form_and_check('${here/getPrimaryUrlPath}')"
        id="dialog_submit"
        type="submit"
        value="Create"
        name="createObject:method"/>
    <input id="dialog_cancel" type="button" value="Cancel"
           onclick="$('dialog').hide()"/>

These examples will cover most cases; generally, a good idea is to look at other dialog templates that contain similar elements or perform similar actions.

6.3. Adding a New Menu or Menu Item

Classes that inherit from the ZenMenuable mixin have a method called getMenus, which traverses up the object's path aggregating ZenMenuItem objects owned by its ancestors. These objects comprise an action to be executed, a human-readable description, and various attributes restricting the objects to which the item is applicable.

For example, imagine basic menus exist on dmd and dmd.Devices:

    dmd
        More              (menu)
            See more...   (menu item)
            Do more...
        Manage
            Manage object...
    dmd.Devices
        More
            See more...
            Do less...

A call to dmd.Devices.getMenus() will return:

    More
        See more...      (from dmd.Devices)
        Do more...       (from dmd)
        Do less...       (from dmd.Devices)
    Manage
        Manage object... (from dmd)

As you can see, menu items inherit their ancestors' unless they define their own, which override when their ancestors' conflict.

In theory, all ZenMenuables (which includes nearly all objects in Zenoss) may own menu items; in practice, all but a few menus live on /zport/dmd.

Adding a new menu item is fairly straightforward. Because menu items are persistent objects, modifications must happen in a migrate script (or be included as XML in a ZenPack). The method ZenMenuable.buildMenus() accepts a dictionary of menus, each of which is a list of dictionaries representing the attributes of menu items. Instructions on writing migrate scripts can be found elsewhere in this guide.

  1. Find the id of the menu to which you wish to add items. The simplest way to do this is to locate the menu_ids definition on the page template that renders the menu. Tables will have a single menu id. The page menu may have several, which will be rendered as sub-menus. The TopLevel menu is a special case; it appears in the page menu, but its items are rendered as siblings of the other menus.

  2. If activating the menu item will require a dialog, create one. See the Section 6.2, “Adding a Dialog” section of this guide for more info.

  3. Determine the objects for which the menu item should be visible. Menu items will use several criteria for determining whether to apply:

    • allowed_classes: A list of strings of class names for which the menu item should be rendered.

    • banned_classes: A list of strings of class names for which the menu item should not be rendered.

    • banned_ids: A list of strings of object ids for which the menu item should not be rendered.

    • isglobal: Whether the menu item should be inherited by children of the menu item's owner.

    • permissions: The permissions the current user must have for the context in order for the item to render.

  4. Figure out the action the menu item will perform. If it's a dialog, then the action is the name of the dialog template, and the isdialog attribute of the menu item should be True. If it's a regular link, the action should be the URL or "javascript:" you would normally have as the href attribute of an anchor.

  5. Now build the dictionary. It should look like this, where MenuId is the menu from step 1:

        menus = { 'MenuId': [
                    { 'id': 'myUniqueId',
                      'description': 'Perform My Action...',
                      'action': 'dialog_myAction',
                      'isdialog': True,
                      'allowed_classes': ('MyGoodClass',),
                      'banned_classes': ('MyBadClass',),
                      'banned_ids': ('Devices',),
                      'ordering': 50.0,
                      'permissions': (ZenossSecurity.ZEN_COMMON,)
                    },
                ]}

    'ordering' is a float determining the item's relative position in the menu. Greater numbers mean the item will be placed higher. Also notice that it's almost certainly pointless to set both allowed_classes and banned_classes; it was done here only as an example. The permission ZEN_COMMON is a standard Zenoss permission -- see the new permissions section of this guide for more information.

    If you have more menu items in the same menu, you can add them to that list; if you have more menus, you can create more keys in the menus dictionary.

  6. Finally, use the dmd.buildMenus() method to create the MenuItems:

        dmd.buildMenus(menus)

6.4. Creating a Table Using ZenTableManager

ZenTableManager is a Zope product that helps manage and display large sets of tabular data. It allows for column sorting, breaking down the set into pages, and filtering of elements in the table.

Here's a sample of a table listing all devices under the current object along with their IPs. First we set up the form that will deal with our navigation form elements:

 ...
<form method="post" tal:attributes="action here/absolute_url_path" 
   name="[MYFORM]">
<script type="text/javascript" 
    src="/zport/portal_skins/zenmodel/submitViaEnter.js"></script>

Next, we set up our table, defining the objects we want to list (in this case, here/devices/getSubDevicesGen). We then pass those objects, along with a unique tableName, to ZenTableManager, which will return a batch of those objects of the right size (for paging purposes):

  <table class="zentable"
tal:define="objects here/devices/getSubDevicesGen;
tableName string:myDeviceTable;
batch python:here.ZenTableManager.getBatch(tableName, objects)"
tal:condition="python:batch or
here.ZenTableManager.getTableState(tableName, 'filter')">

Next, a table header and a couple of hidden fields:

 <tr>
<th class="tabletitle" colspan="2"> <!--Colspan will of course change with the number of fields you show-->
My Devices
</th>
</tr>
<input type='hidden' name='tableName' tal:attributes='value tableName' />
<input type='hidden' name='zenScreenName' tal:attributes='value template/id' />

Now we add the rows that describe our devices. First we need to set up the column headers so that they'll be clickable for sorting. For that, we use ZenTableManager.getTableHeader(tableName, fieldName, fieldTitle, sortRule="cmp").

 <tbody>
<tr>
<!--We want to sort by names using case-insensitive comparison-->
<th tal:replace="structure python:here.ZenTableManager.getTableHeader(
tableName, 'primarySortKey', 'Name', 'nocase')">name</th>
<!--Default sortRule is fine for IP sorting-->
<th tal:replace="structure python:here.ZenTableManager.getTableHeader(
tableName, 'getDeviceIp', 'IP')">ip</th>
</tr>

Now the data themselves. In order to have our rows alternate colors, we'll use the useful TALES attribute odd, which is True for every other item in a tal:repeat loop.

 <tal:block tal:repeat="device batch">
<tr tal:define="odd repeat/device/odd"
tal:attributes="class python:test(odd, 'odd', 'even')">
<td class="tablevalues">
<a class="tablevalues" href="href"
tal:attributes="href device/getDeviceUrl"
tal:content="device/id">device
</a>
</td>
<td class="tablevalues"
tal:content="device/getDeviceIp">ip</td>
</tr>
</tal:block>
</tbody> 

Finally, let's add the navigation tools we need and close off our tags.

 <tr>
<td colspan="2" class="tableheader">
<span metal:use-macro="here/zenTableNavigation/macros/navbodypagedevice" />
</td>
</tr>

</table>
</form> 

6.5. Creating an Editable Table

But what if you want to be able to edit devices from this table? The process is simple. First, you add a checkbox to the first column of your device list:

 <td class="tablevalues" align="left">
<!--Now add your checkbox, defining the list of devices as "deviceNames"-->
<input tal:condition="here/editableDeviceList"
type="checkbox" name="deviceNames:list"
tal:attributes="value device/getRelationshipManagerId"/>
<!--Then the first column contents as above-->
<a...>device</a>
</td> 

Now that we can choose devices from the list, we need the controls to edit them. In this case, we'll use a macro defining controls that allow a device to be moved to a different device class. Just add the macro call to the end of your table:

 ...
</tr>
<!--Add controls here-->
<tal:block tal:condition="here/editableDeviceList"
tal:define="numColumns string:5"> <!--This macro includes the <tr> tag, so we need to pass it colspan-->
<span metal:use-macro="here/deviceListMacro/macros/deviceControl" />
</tal:block>

</table>
</form>

6.6. How to Save Properties via an Edit Screen

Creating a new Edit Form.

Add form input fields

Add a boolean type:

 ...
<select class="tablevalues"
tal:attributes="name MyBooleanProperty:boolean">
<option tal:repeat="boolProp python:(True,False)" tal:content="boolProp"
tal:attributes="value boolProp; 
          selected python:boolProp==here.getMyBooleanProperty()"/>
</select>
...

This block of code creates a select dropdown with two options: True and False. The select dropdown is pre-populated with the value returned by getMyBooleanProperty(). The value of this form field will be stored in the attribute MyBooleanProperty.

Add a text box type:

 ...
<textarea class="tablevalues" rows='5' cols="33"
tal:attributes="name MyTextProperty:text"
tal:content="here/getMyTextProperty">
</textarea>
...

This block of code creates a text box. The text box is pre-populated with the string value returned by getMyTextBoxProperty(). The value of this form field will be stored in the attribute MyTextBoxProperty.

Add a text type:

 ...
<input class="tablevalues" type="text" size="40"
tal:attributes="value here/getMyStringProperty; name MyStringProperty"/>
...

This block of code creates a text field. The text field is pre-populated with the string value returned by getMyStringProperty(). The value of this form field will be stored in the attribute MyStringProperty.

Add a select dropdown type:

 ...
<select class="tablevalues"
tal:attributes="name MySelectProperty">
<option tal:repeat="propOption here/getMySelectPropertyOptions"
tal:content="propOption"
tal:attributes="value propOption; 
     selected python:propOption==getMySelectProperty()" />
</select>
...

This block of code creates a select dropdown where the option value and displayed option string are the same. A list of option values are returned by getMySelectPropertyOptions. The select dropdown is pre-populated by the value in getMySelectProperty. The value of this form field will be stored in the attribute MySelectProperty.

 ...
<select class="tablevalues"
tal:attributes="name MySelectProperty:int">
<option tal:repeat="propOptionTuple here/getMySelectPropertyOptionTuples"
tal:content="python:propOptionTuple[0]"
tal:attributes="value propOptionTuple[1]; 
    selected python:propOptionTuple[1]==getMySelectProperty()" />
</select>
...

This block of code creates a select dropdown where the option value is an integer and displayed option is a string. A list of tuples containing the option values and displayed option string are returned by getMySelectPropertyOptionTuples. The select dropdown is pre-populated by the value in getMySelectProperty. The value of this form field will be stored in the attribute MySelectProperty.

Add the form action

...
<form id='MyForm' method="post" tal:attributes="action here/absolute_url_path">
...

The form action should be set to a function (i.e. here/absolute_url_path) that returns the path to the object being edited.

 ...
<input class="tableheader" type="submit"
name="saveProperties:method" value=" Save " />
...

This submit button name will be in the format saveProperties:method. saveProperties is the method name that will be executed when the submit button is clicked.

Add the save() method

...
def saveProperties(self, REQUEST=None):
	"""Save all Properties found in the REQUEST.form object. """
	for name, value in REQUEST.form.items():
		if getattr(self, name, None) != value:
			self.setProperty(name, value)

	return self.callZenScreen(REQUEST)
...

Create a saveProperty() method in the effective object.