Refreshing content with KSS
The goal
You are looking at a page on your Plone Site. You change the review state of the current object. Without a complete page reload you want some bits of the page to change automatically. Plone itself is doing that already. You want to have that kind of fun too!
The use case
eXtremeManagement is a project management tool, based on eXtreme Programming practices. We will skip details and just say that you can add a content type Story to your Plone Site. Within this Story you can add a content type Task. Stories start out in the workflow review state concept. In that state no Tasks can be added. The process is:
- You give the Story an initial estimate.
- You set the review state to estimated.
- You add a Task.
For step three, a form is shown when the user viewing that Story has permission to add a Task there. What we want to achieve in this how-to is to display that form automatically at the moment the Story is transitioned to the right state, without needing a full page refresh.
The current state
Step one (giving the story an initial estimate) is simply done on the edit tab.
Step two (changing the review state) is more interesting. In Plone 3 a workflow transition is done using KSS: instead of submitting a form and reloading the entire page, a KSS action is triggered. This takes care of the actual workflow transition. And it refreshes the content menu updating the workflow drop-down and for example the list of content types that are allowed to be added here.
A few more parts of the page are being updated too: the navigation portlet and the recent items portlet. That is done slightly differently: with zope 3 event handlers. That is what we will be doing too.
Step three (adding a Task) is done with a form. The form has already been made. It is available as viewlet on the Story page. It is shown or hidden depending on the current review state. But the showing and hiding currently only happens on a complete page reload.
The basis
First, the best way to refresh a part of the page, is to use something that KSS already knows how to refresh. KSS has the zope command set that contains these methods:
-
refreshProvider(selector, name):
Refresh a page provider with the given name at the selected node.
-
refreshViewlet(self, selector, manager, name):
Refresh a viewlet with the given manager and name at the selected node.
In our case we have a viewlet, so we will be using the refreshViewlet method. We will not show what steps are needed to actually create a viewlet: that is outside the scope of this how-to.
As you can see, those two refresh methods both require a selector argument. This is a class or id identifying a part of your page in the html. It has the same notation as it would have in a CSS file, so .class and #id.
To give an example, when you say:
refreshProvider('#some-id', 'plone.belowcontentbody')
this is taken by KSS to mean:
- Render the plone.belowcontentbody provider (or viewlet manager).
- Place the rendered html in the html element with id some-id. The original element is completely replaced.
What this means, which is my second point, is that it is handy to let that id (some-id) be rendered by the viewlet, also when the page is first loaded. That way the viewlet always renders something and is responsible itself for making sure that an element with that id is present at the page, otherwise KSS cannot find a target to replace. An example works best. So how does it look in the case of our add task form? Observe:
<div id="add-task"
tal:define="story_view context/@@story">
<span
style="display:none"
tal:condition="not:story_view/show_add_task_form">
No task add form.
</span>
<tal:addtask
condition="story_view/show_add_task_form">
<form name="add_task"
...
</form>
</tal:addtask>
</div>
Never mind about the details of the @@story view that you see being called. The important thing is that the div with our id (add-task) is always shown, even when the form itself is not always shown. There are certainly other ways to do this, but you can use this as a start for when you want to do this yourself.
The implementation
In the browser directory we add a file xm_kss.py. It starts out like this:
from zope.component import adapter
from kss.core.interfaces import IKSSView
from Products.DCWorkflow.interfaces import IAfterTransitionEvent
from Products.eXtremeManagement.interfaces import IXMStory
@adapter(IXMStory, IKSSView, IAfterTransitionEvent)
def story_workflow_changed(obj, view, event):
if not (event.old_state is event.new_state):
viewletReloader = ViewletReloader(view)
viewletReloader.update_story_viewlets()
So we see some imports and a function. The function is an adapter. We need a subscriber line in configure.zcml for that too:
<configure
xmlns="http://namespaces.zope.org/zope">
<subscriber handler=".xm_kss.story_workflow_changed"/>
</configure>
In combination with the @adapter line this subscribes the handler to the event that is fired after transitioning (IAfterTransitionEvent) a Story (IXMStory) within a KSS view (IKSSView). In this how-to we do not care where and how that event gets fired. We just accept that it happens and we are glad.
With that out of the way, let us look at that function. It first checks whether the state has actually changed. If that is the case, then it creates an instance of the ViewletReloader class, with the KSS view passed as argument. So where does that class come from? It is in the same file and looks like this:
class ViewletReloader(object):
"""Reload xm viewlets that depend on the workflow state.
"""
def __init__(self, view):
self.view = view
self.context = view.context
self.request = view.request
def update_story_viewlets(self):
"""Refresh story viewlets.
"""
zope = self.view.getCommandSet('zope')
zope.refreshViewlet('#add-task',
'plone.belowcontentbody',
'xm.add_task_form')
The view in the init method is the KSS view responsible for firing the event. We take the context and request from that view and store them on the class. Actually in our case this is not needed, but it can be handy for your own use case.
As you may have seen, in the event handler we call the update_story_viewlets method of this class. In that method we get the zope command set and refresh a viewlet. Going through the supplied arguments passed to refreshViewlet in order:
- We select the html element with id add-task and replace it.
- We replace it with some content from the viewlet manager plone.belowcontentbody.
- Specifically, we replace it with the viewlet xm.add_task_form.
The summary
So here is all that we really needed:
- a viewlet that always at least prints an html element with a unique id;
- an adapter (with zcml), which is really a small function that calls a helper class;
- a helper class that takes a KSS view as parameter and has a small method that tells KSS which content to refresh.
eXtremeManagement has been KiSSed! How about you?