Personal tools
You are here: Home   Additional documentation   Development process with KSS  
Search
 
Document Actions

Development process with KSS

by Godefroid Chapelle last modified 2006-09-02 23:22

Steps taken to go from a standard Zope/Plone tool to its ajaxified version.

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='/index.html'.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 macro that I can reuse.

kss_addBookmark.py

from Products.PloneAzax import AzaxBaseView

view = AzaxBaseView(context, context.REQUEST)
view.executeMacro('issuePortalMessage', 'test')
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 Products.PloneAzax import AzaxBaseView

message = context.doBookmark()

view = AzaxBaseView(context, context.REQUEST)
view.executeMacro('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 macro available.

from Products.PloneAzax import AzaxBaseView

message = context.doBookmark()

view = AzaxBaseView(context, context.REQUEST)
view.executeMacro('issuePortalMessage', message)
view.executeMacro('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 azax product. BTW, as good readers have found out by themselves, AzaxBaseView is a Five view.

Related content