The tool we build is a simple bookmarking tool : on a page, you get a supplementary link in the personal bar which adds a bookmark to your personal folder. The five most recents bookmarks are shown in a portlet. 

As the aim of this page is to describe the development process of ajaxification, the tool itself is not much refined : it stores the bookmarks right in the personal folder as ATLinks.

Step 0
======

The *non-Ajax* tool consists of two very simple pieces of code : a script to add the
bookmark and a portlet to display the bookmarks.

The two pieces can be easily added in the custom layer of portal_skins in a Plone
site (BTW, this shows how a few lines of code can really build nice
functionality in our platform). Note that if you add the files TTW, you must omit the file
extensions, except for the kss file where it is more correct to postfix the name with ``.kss``.

The script is setup as a user action so that it appears in the personal bar.

addBookmark.py
--------------

The script checks if the bookmark exists (well - if a
bookmark with the same id exists) and creates it if not::

    from Products.CMFCore.utils import getToolByName
    from Products.CMFPlone import PloneMessageFactory as _

    pm = getToolByName(context, 'portal_membership')
    memberArea = pm.getHomeFolder()

    bookmark = None
    if not context.getId() in memberArea.objectIds():
        memberArea.invokeFactory('Link', context.getId())
        bookmark = getattr(memberArea, context.getId())
        bookmark.setTitle(context.Title())
        bookmark.setRemoteUrl(context.absolute_url())
        bookmark.reindexObject()

    if bookmark is None:
        message = _(u'Bookmark already there')
    else:
        message = _(u'Bookmark added')

    putils = getToolByName(context, 'plone_utils')
    putils.addPortalMessage(message)

    context.REQUEST.RESPONSE.redirect(context.absolute_url())

portlet_bookmarks.pt
--------------------

Based on ``portlet_news``, only the query has been adapted::

    <html xmlns:tal="http://xml.zope.org/namespaces/tal"
          xmlns:metal="http://xml.zope.org/namespaces/metal"
          i18n:domain="plone">
    <body>
    <div metal:define-macro="portlet"
         tal:condition="not:isAnon">
    <tal:recentlist 
         tal:define="hfolder mtool/getHomeFolder;
                     results python:context.portal_catalog.searchResults(
                         sort_on='modified',
                         path='/'.join(hfolder.getPhysicalPath()),
                         portal_type='Link',
                         sort_order='reverse',
                         sort_limit=5)[:5];
    ">


    <dl class="portlet" id="portlet-bookmarks">

        <dt class="portletHeader">
            <span class="portletTopLeft"></span>
            <a tal:attributes="href hfolder/absolute_url"
               i18n:translate="box_bookmarks">Bookmarks</a>
            <span class="portletTopRight"></span>
        </dt>
        <tal:items tal:repeat="obj results">
        <dd class="portletItem"
            tal:define="oddrow repeat/obj/odd;
                        item_wf_state obj/review_state;
                        item_wf_state_class python:'state-' + normalizeString(item_wf_state);
                        item_type_class python: 'contenttype-' + normalizeString(obj.portal_type);"
            tal:attributes="class python:test(oddrow,
                                             'portletItem even',
                                             'portletItem odd')">
            <div tal:attributes="class item_type_class">
            <a href=""
               tal:attributes="href string:${obj/getURL}/view;
                               title obj/Description;
                               class string:$item_wf_state_class visualIconPadding tile">
                <tal:title content="obj/pretty_title_or_id">
                Plone 2.1 released!
                </tal:title>
                <span class="portletItemDetails"
                      tal:content="python:toLocalizedTime(obj.ModificationDate)"
                      >May 5</span>
            </a>
            </div>
        </dd>
        </tal:items>

        <dd class="portletItem"
            tal:condition="not: results"
            i18n:translate="box_bookmarks_no_items">
            No items bookmarked yet.
        </dd>

        <dd class="portletFooter">
            <span class="portletBottomLeft"></span>
            <span class="portletBottomRight"></span>
        </dd>
    </dl>

    </tal:recentlist>

    </div>
    </body>
    </html>



Step 1
======

Now, let's start the Ajax part.

The dynamic comportment envisioned is the following :
when clicking on the action link in the personal bar, instead of refreshing the whole page which would
include the portal message and an updated portlet, let's only add the
portal message and refresh the given portlet. 

To setup a click event on the link in personal bar, we need to add an HTML id
on it, so that we can select it with a kss rule. This is the reason why we need
to modify the ``global_personalbar`` template.


global_personalbar.pt
---------------------

Let's customize ``global_personalbar.pt`` : I add
``id string:${action/category}-${action/id}`` in
``tal:attributes``. Combining action category and id should ensure it is
unique. ::

    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
          i18n:domain="plone">

    <body>

    <!-- THE PERSONAL BAR DEFINITION -->

    <div metal:define-macro="personal_bar"
          id="portal-personaltools-wrapper"
          tal:define="display_actions python:user_actions[:-1]+global_actions+user_actions[-1:];
                      getIconFor nocall:putils/getIconFor;">

    <h5 class="hiddenStructure" i18n:translate="heading_personal_tools">Personal tools</h5>

    <ul id="portal-personaltools">
       <tal:block condition="not: isAnon">
           <li class="portalUser" 
               tal:define="author python:mtool.getMemberInfo(user.getId())"><a 
               id="user-name"
               tal:attributes="href string:${portal_url}/author/${user/getId}">
               <span class="visualCaseSensitive"
                     tal:content="python:author and author['fullname'] or user.getId()">
                    John
               </span>
           </a></li>
       </tal:block>

        <tal:actions tal:repeat="action python:here.getOrderedUserActions(keyed_actions=keyed_actions)">
            <li tal:define="icon python:getIconFor(action['category'], action['id'], None);
                            class_name string:actionicon-${action/category}-${action/id};
                            class_name python:test(icon, class_name, nothing);"
                tal:attributes="class class_name">
                <a href=""
                   tal:attributes="href action/url;
                                   class python:test(icon, 'visualIconPadding', nothing);
                                   id string:${action/category}-${action/id}">
                   <tal:actionname i18n:translate="" tal:content="action/title">dummy</tal:actionname>
                </a>
            </li>
        </tal:actions>

    </ul>
    </div>

    </body>
    </html>

I also add the action through the ``portal_membership`` tool, with id ``action``,
URL ``string:${object_url}/addBookmark`` and category ``user``. After this the
bookmark link will appear in the personal toolbar and if clicked, it will 
bookmark the current page. It can also be checked that the html tag of the link has
``id=user-bookmark`` set on it. 

Step 2
======

I put a rule in a kss stylesheet to bind the click event of the ``bookmark`` link.
``action-client: alert`` is an easy way of checking that the event is actually
bound.

The kss stylesheet needs to be registered in ``portal_css``. kss stylesheets need 
to be setup as ``link`` and with rel as ``k-stylesheet``.

bookmarks.kss
-------------

::

    #user-bookmark:click {
        action-client: alert;
    }

With this code, when I click the link, I get an alert showing that the event is
bound... and the normal flow goes on with a full refresh of the page.

Step 3
======

I need to disable the call to the ``addBookmark.py`` script (and its
consequence, page reload).

bookmarks.kss (2)
-----------------

A parameter is added to the event : ``preventdefault``. ::

    #user-bookmark:click {
        evt-click-preventdefault: True;
        action-client: alert;
    }

Step 4
======

I now need the client to call the server when the link is clicked. We will get back
a set of commands that will, among others, update the portal message.

bookmarks.kss (3)
-----------------

By changing the action, I ensure that ``kss_addBookmark`` will be called on the
server::

    #user-bookmark:click {
        evt-click-preventdefault: True;
        action-server: kss_addBookmark;
    }

I need to create a script that will cook a response that kss can interprete to
update the page. Let's first update the portal message. I am lucky that the
portal message code already exists as a command that I can reuse.

kss_addBookmark.py
------------------

::

    from plone.app.kss import AzaxBaseView
    
    from Products.CMFPlone import PloneMessageFactory as _
    message = _('test')

    view = AzaxBaseView(context, context.REQUEST)
    view.getCommandSet('portalmessage').issuePortalMessage(message)
    return view.render()
    
When clicking the link now, I'll get a portal message saying ``test``.

Step 5
======

We will now modify ``kss_addBookmark.py`` so that it actually stores a bookmark.

To avoid code duplication, let's refactor ``addBookmark.py``.
We want to insulate in ``doBookmark.py`` the code that does the computation.

This way, we will be able to share code between the Ajax and non-Ajax
coexisting parts of the code.

doBookmark.py
-------------

::

    from Products.CMFCore.utils import getToolByName
    from Products.CMFPlone import PloneMessageFactory as _

    pm = getToolByName(context, 'portal_membership')
    memberArea = pm.getHomeFolder()

    bookmark = None
    if not context.getId() in memberArea.objectIds():
        memberArea.invokeFactory('Link', context.getId())
        bookmark = getattr(memberArea, context.getId())
        bookmark.setTitle(context.Title())
        bookmark.setRemoteUrl(context.absolute_url())
        bookmark.reindexObject()

    if bookmark is None:
        message = _(u'Bookmark already there')
    else:
        message = _(u'Bookmark added')

    return message


addBookmark.py (2)
------------------

::

    from Products.CMFCore.utils import getToolByName

    message = context.doBookmark()

    putils = getToolByName(context, 'plone_utils')
    putils.addPortalMessage(message)

    context.REQUEST.RESPONSE.redirect(context.absolute_url())

kss_addBookmark.py (2)
----------------------

We can now actually call the bookmarking code and issue the computed portal
message. ::

    from plone.app.kss import AzaxBaseView
                                                                                  
    message = context.doBookmark()

    commands = AzaxBaseView(context, context.REQUEST)
    commands.getCommandSet().issuePortalMessage(message)
    return view.render()

Step 6
======

The only thing still missing is the refreshing of the portlet.

kss_addBookmark.py (3)
----------------------

As portlets are obvious parts of the Plone UI, there is also already a command
available to refresh one of them. ::

    from plone.app.kss import AzaxBaseView
                                                                                  
    message = context.doBookmark()

    commands = AzaxBaseView(context, context.REQUEST)
    commands.getCommandSet('portalmessage').issuePortalMessage(message)
    commands.getCommandSet('refreshportlet').refreshPortlet('bookmarks')
    return view.render()

All this development was done TTW in order to avoid all skeleton code crufts.
It should be obvious that all this can (should) happen on the file system. In
particular, KSS responses can be built with Z3 tools available in the ``kss``
product. BTW, as good readers have found out by themselves, ``AzaxBaseView`` is a
Five view.

