wiki:S3/S3Navigation

S3Navigation

S3Navigation is an S3 module providing a generic framework for the implementation of navigation elements in the web UI.

S3NavigationItem

S3NavigationItem is a base class for the implementation of navigation items, and provides the common logic for all types of navigation items as well as an API for renderers.

Navigation items are UI elements to navigate the application, and can appear as menus, breadcrumb rows, tab rows, and various other types of links. They all have in common that they are linked to request URLs and usually get activated by clicking them.

Subclassing S3NavigationItem

The S3NavigationItem class is not meant to be used directly for the implementation of navigation items. Instead, it should be subclassed for each type of navigation items the user interface needs. Subclasses of S3NavigationItem are supposed to implement the layout of the respective item type, and are therefore referred to as Layouts.

To implement a Layout, create a new subclass of S3NavigationItem and implement a (static) method layout:

class MyMenuLayout(S3NavigationItem):

    @staticmethod
    def layout(item):
        # Layout implementation goes here

The layout method takes an instance of the layout class as parameter, and returns an instance of a web2py HTML helper class (DIV or a subclass of it). This HTML instance is what eventually gets written into the view.

The layout method can make use of the renderer API to retrieve the item's attributes and flags.

Defining Navigation Items

Once you have implemented the Layout, you can create instances of it to define the particular navigation elements. The elements can be nested using the call-method:

my_menu = MyMenuLayout("Home")(
             MyMenuLayout("First Item"),
             MyMenuLayout("Second Item"),
             MyMenuLayout("Submenu")(
                 MyMenuLayout("First SubmenuItem"),
                 MyMenuLayout("Second SubmenuItem")
             )
           )

Each navigation item can receive any number of keyword parameters.

The following keyword parameters are used by the base-class:

KeywordTypeMeaningComments
labelstringThe label for the item (if any)always the first parameter, so the keyword can be omitted; labels which are not already T instances (localized strings) will automatically be rendered with T unless you set translate=False
rRequestThe request objectif passed, then the item inherits a, c, and f from this request, i.e. the item will always match this request
astringThe application name in the target URLinherited from parent item
cstringThe controller name in the target URLinherited from parent item
fstringThe function name in the target URLinherited from parent item
argslist of strings, or stringthe arguments in the target URLif passed as single string, the arguments must be separated by slashes
varsdict of stringsthe query variables in the target URL
extensionstringthe URL extension (data format extension) in the target URLdefaults to None
pstringthe permission required to access this itemdefaults to m
mstringthe URL method for the requestdefaults to "", will be appended to args if not the last arg
tstringthe name of the table the permission is required fordefaults to <c>_<f>
tagslist of stringsa list of tags for this itemtags can later be used to address this item in the menu tree, e.g. to update flags
parentthe parent itemset the parent item explicitlyspecial purpose, defaults to None and should be left at that
translatebooleanFalse will prevent automatic localization of the item label
layoutfunctionuse this function to render this item instead of the layout of the class
checkboolean, function or mixed list of these typesconditions to check before renderingfunctions will be called with the item as parameter and are expected to return a boolean
restrictlist of role IDs or UIDsrestrict access to this item to these roles
linkbooleanwhether to link this item to the specified target URL or notdefaults to True = all items get linked
mandatorybooleanoverride active check - item is always activenot implemented yet

All other keyword parameters will be stored in the item instance as they are and get passed on to the layout method. Keywords starting with an "_" underscore will be stored in the item.attr dict, all other keywords in the item.opts dict. It is up to the layout method to deal with those keyword parameters then.

Status Flags

During the status check cascade, each navigation item receives a set of status flags which then are available to the layout method. It is up to the layout method whether and how each combination of flags influences the rendering of the item.

FlagMeaningComments
item.enabledThe item shall be rendered as enabled (accessible)renderers would usually skip items with enabled=False, but could also render them greyed-out or otherwise visible but inaccessible; this flag will be altered by the return value of the check-hooks, and can be overridden by the check_enabled method of the class
item.selectedThe target URL of the item or any of its sub-items matches the current requestthis can be used for menu highlighting; if more than one subtree can be matched, only items in the first matching subtree will be marked selected
item.authorizedThe user is permitted to access the target URL of this itemrenderers would usually skip items the user is not permitted to access, but could also render the item greyed-out or otherwise visible but inaccessible
item.visibleCustom flag to show/hide itemswill always be True unless explicitly changed by a check-hook method

Status Check Cascade

All S3Navigation items are run through a cascade of status checks before they get handed over to the layout method.

The default behavior of these methods is meant for menu items, which may though not fit for all types of navigation items. Where needed, the subclass should overwrite them accordingly. These methods won't take any parameter beyond self and are expected to return a boolean value. The methods are called in the order they appear in the following table:

MethodMeaningEffectFlag setStandard Behavior
check_activeCheck whether the item is relevant for the current requestif this returns False, the item will be deactivated and no further checks nor the renderer ever be invoked (abort) check_active returns False if a target controller is defined, but disabled in deployment settings, or if neither the item nor any of the items in the same subtree matches the request at least with its controller attribute*
check_permissionCheck whether the user is permitted to access the target URL of the itemsets flagauthorizedreturns True if an auth.accessible_url can be created from the target URL
check_selectedCheck whether the item matches the current requestsets flag upward in the whole subtreeselectedreturns True if this item or any of its children, out of all items in the tree, reaches the highest match-level for the current request
check_hookRuns the check-hook methods defined for this itemif this returns False, neither check_enabled nor the renderer will be run returns True only if all check-hooks return True, otherwise False
check_enabledCheck whether the item is enabledOverrides the enabled flag if (and only if) it returns False, if it returns True, the flag remains unchangedenabledby default, check_enabled returns always True

!* Note that c=None or f=None match any controller/function

Implementing Layouts

Subclasses of S3NavigationItem must implement the layout method:

class MyMenuLayout(S3NavigationItem):

    @staticmethod
    def layout(item):
        # Layout goes here

Usually, this should be a static method. It will be called with an instance of the same class, and is expected to return a web2py HTML helper instance (i.e. a subclass of DIV, which implements the xml method).

S3NavigationItem provides an API for the layout method (=renderer) to introspect the item before rendering.

Item Attributes

AttributeTypeContentsComments
labelT instance or strthe localized label for this itemcan be None for container items
controllerstringcontroller name of the target URLdon't use directly, use get method instead
functionstringfunction name of the target URLdon't use directly, use get method instead
applicationstringapplication name of the target URLdon't use directly, use get method instead
rrequest objectthe request this item was matched againstdefaults to current.request
argslist of stringsthe args for the target URL
varsdict of stringsthe query variables for the target URL
extensionstringthe format extension for the target URL
tablenamestringthe name of the target table of the target URL
attrdictthe HTML attributesas passed during item definition
optsdictthe renderer optionsas passed during item definition
authorizedFlagwhether the current user is permitted to access the target URL
enabledFlagwhether the item is enabled
visibleFlagwhether the item shall be visiblerenderers should always respect this flag
selectedFlagwhether the item belongs to the currently selected menu path
parentnavigation itemthe parent itemNone if this is a top-level item
componentslist of navigation itemsall sub-items for this itemempty list if this is a leaf item

Introspection Methods

S3NavigationItem provides a number of helper methods for item introspection:

Get a sub-item of this item at position i:

    sub_item = item[i]

Loop over sub-items:

    for sub_item in item:
        ...

Get the root item of the tree:

    root_item = item.get_root()

Get the ancestor path (list of items) from the root item down to this item:

    path = item.path()

Get all sub-items with certain flags (can also take multiple flags):

    enabled_sub_items = item.get_all(enabled=True)

Get the first sub-item with certain flags (can also take multiple flags):

    first_authorized_and_enabled_sub_item = item.get_first(enabled=True, authorized=True)

Get the last sub-item with certain flags (analogous to above):

    last_authorized_and_enabled_sub_item = item.get_last(enabled=True, authorized=True)

Number of sub-items:

    number_of_subitems = len(item)

Position of a sub-item within the sub-items list:

    sub_item_position = item.index(sub_item)

Position of this item within the parent's sub-items list:

    my_position = item.pos()

Check whether this is the first sub-item with certain flags:

    is_first_enabled = item.is_first(enabled=True)

Check whether this is the last sub-item with certain flags:

    is_last_authorized_and_visible = item.is_last(authorized=True, visible=True)

Get all preceding siblings of this item within the parent (as list):

    preceding_siblings = item.preceding()

Get all following siblings of this item within the parent (as list):

    following_siblings = item.following()

Get the last preceding item with certain flags:

    preceding_enabled = item.get_prev(enabled=True)

Get the first following item with certain flags:

    following_visible_and_enabled = item.get_next(visible=True, enabled=True)

Get the item URL:

    url = item.url()

Get an inherited attribute (i.e. controller, function or application; these attributes should never be accessed directly):

    controller = item.get("controller")

Check whether this item or any item underneath it contains a certain tag:

    tag = "special"
    if tag in item:
        # This item or at least one underneath it contains this tag

Manipulation methods

These methods can be used by controllers to influence the rendering of items:

Get all items underneath this item with a certain tag (as list):

    tag = "special"
    special_items = item.findall(tag)

Enable/disable items by tag:

    tag = "special"
    item.enable(tag) # enables all descendand items with the tag "special"

    tag = "nonspecial"
    item.disable(tag) # disables all descendand items with the tag "nonspecial"

If enable or disable are used without tag, then only the current item gets enabled/disabled

Set layout:

    tag = "special"
    def f(item):
        # Layout goes here

    # Set this layout to all descendand items with the tag "special"
    item.set_layout(f, tag=tag, recursive=True)

Rendering Methods

Instead of stepping through the sub-items and recursively rendering the descendant tree, the layout method (=renderer) can use:

    sub_items = item.render_components()

This will status-check and render all sub-items in the correct order, and return the result as a list of web2py HTML helper instances.

Note that the layout method should never render sub-items itself: they could be of a different class! Always run the sub-item's render() method in order to have it properly status-checked and rendered, or even better use render_components() of the current item.

Invoking the Renderer

To render a navigation element (i.e. to run the layout method), just put it into the view like:

{{=my_menu}}

This will call the layout method of the element, provided that the element is active (i.e. relevant for the current request).

Eden Navigation Layouts

The trunk version of Sahana Eden currently defines 4 different layouts (S3NavigationItem subclasses):

  • S3MainMenuLayout (Shortcut MM) for the application main menu (also called "modules" menu)
  • S3OptionsMenuLayout (Shortcut M) for the options menu (also called "controller" menu)
  • S3MenuSeparatorLayout (Shortcut SEP) for separators in pull-down menus
  • S3BreadcrumbsLayout for breadcrumbs

These layout classes (as well as additional layouts) are defined in modules/s3layouts.py and can be customized there.

Customizing Eden Menus

For the trunk version of Sahana Eden, all menu definitions have been implemented as functions in two classes:

  • S3MainMenu for the application main menu ("modules" menu)
  • S3OptionsMenu for the options menus per module ("controller" menus)

These classes can be found in modules/s3menus.py and shall not be customized in place - instead, any custom menu definitions can happen in modules/templates/<template>/menus.py.

Custom Modules Menu

models/00_utils.py defines the application main menu ("modules" menu). In the trunk version, this invokes the respective definition methods of the S3MainMenu class in modules/s3menus.py.

To customize the main menu, you should create a menus.py in your modules/templates/<template> folder.

Old: you can simply comment the respective standard option and replace it by a custom menu definition, e.g.:

    # =========================================================================
    # Main menu
    #
    current.menu.main(

        # Standard modules-menu, commented out
        #S3MainMenu.menu_modules(),

        # Custom menu:
        homepage(), # no parameters -> links to default/index
        homepage("gis"),
        homepage("pr", restrict=[ADMIN])(
            MM("Persons", f="person"),
            MM("Groups", f="group")
        ),
        MM("more", link=False)(
            homepage("dvi"),
            homepage("irs")
        ),

        # Standard service menus
        S3MainMenu.menu_help(right=True),
        S3MainMenu.menu_auth(right=True),
        S3MainMenu.menu_lang(right=True),
        S3MainMenu.menu_admin(right=True),
        S3MainMenu.menu_gis(right=True)
    )

This example uses the homepage helper function to define a MM instance out of the module's nice name setting in 000_config.

Mind the trailing commas! Note that you do not need to check for module activation - this happens automatically in check_active: items linked to deactivated controllers will automatically be deactivated.

Custom Options Menus

To customize a controller menu, you can override the standard menu in the s3_menu_dict in models/00_utils.py with a custom menu definition, e.g.:

    # =========================================================================
    # Controller menus
    #
    s3_menu_dict = {

        # Custom menu for the PR controller
        "pr": M(c="pr")(                        # <- the menu itself has no label, just the controller
                  M("Persons", f="person")(     # <- Sub-item, inherits c from the parent item
                       M("List"),               # <- Sub-item, inherits both c and f from the parent item
                       M("New", m="create"),    # <- method m will be appended to args, if not the last arg
                       M("Search", m="search"), # <- no need for T() of the label, happens automatically
                  ),
                  M("Groups", f="group")(
                       M("List"),
                       M("New", m="create"),
                       M("Search", m="search"),
                  ),
               ),
    }

All controller menus which are not defined in s3_menu_dict fall back to the standard definition in S3OptionsMenu in modules/s3menus.py. Instead of the menu definition in place, you can of course also call a custom function to define the menu - this is especially helpful where certain sequences shall be re-used in multiple places.

Options menus which are to be shared by multiple controllers must not define the controller in the root item, but instead at the first sub-item level.

Items which shall match multiple functions can take a list of function names for the parameter f, where the first function name is used to render the URL, i.e.:

   M("Persons", c="pr", f=["person", "index"])

is rendered as /pr/person but matches both /pr/person and /pr/index.

Manipulating Menus

After a menu has been defined, you can modify its attributes in the controller. To access the current menus, use:

current.menu.main

...for the main menu, and

current.menu.options

...for the current controller menu.

Tree manipulation

This can use the same API as the renderer, except that the status flags won't be available at this time yet. See #ImplementingLayouts.

You can use the call-method later to append more items:

# Append another item to the menu
my_menu(MyMenuLayout("Third Item"))

If you want to insert an item at a particular position, use the insert method:

my_menu.insert(0, MyMenuLayout("New First Item"))

You can also remove items from a certain position:

my_menu.pop(0) # Remove "New First Item" again

Note that every item can only ever belong to exactly one parent item. If you append or insert the same item to a different parent item, it will automatically be removed from the original parent:

# CLI example
>>> menu_1 = M("Menu 1")  # define menu 1
>>> menu_2 = M("Menu 2")  # define menu 2
>>> item_1 = M("Item 1")  # define a menu item
>>> menu_1.append(item_1) # append the menu item to menu 1
<S3OptionsMenuLayout:Menu 1 {<S3OptionsMenuLayout:Item 1>}>
>>> menu_2
<S3OptionsMenuLayout:Menu 2>
>>> menu_2.append(item_1) # append the menu item to menu 2
<S3OptionsMenuLayout:Menu 2 {<S3OptionsMenuLayout:Item 1>}>
>>> menu_1
<S3OptionsMenuLayout:Menu 1> # => menu item no longer component of menu 1

Thus, if you want to re-use a sequence of menu items, make their definition a function and call it at each place you need the sequence.

CLI methods for testing

NB: at the CLI, you can also see the item rendered as HTML, by calling:

>>> menu_2.xml()
'<ul id="subnav"><li><div class="hoverable"><a href="/vita/default/index">Item 1</a></div></li><li><div class="hoverable"><a href="/vita/default/index">Item 1</a></div></li></ul>'

Note that this performs status checks, i.e.:

>>> menu_3 = M("Menu 3", c="pr")
>>> menu_3.xml()
''                 # <- CLI has controller="default", function="index", i.e. this menu item is inactive

It does also perform authorization checks, i.e.

>>> current.request.controller = "pr"
>>> menu_3 = M("Menu 3", c="pr")
>>> menu_3.xml()
''                        # <- Item is active, but unauthorized
>>> menu_3.check_permission()
False
>>> auth.override = True
>>> menu_3.xml()
'<ul id="subnav"></ul>'   # <- Now we're allowed to access this item
>>> auth.override = False
>>> menu_3.xml()
''
Last modified 10 years ago Last modified on 01/05/15 14:57:32
Note: See TracWiki for help on using the wiki.