[Checkins] SVN: Products.CMFDefault/trunk/Products/CMFDefault/ - fixed some search view issues
Yvo Schubbe
cvs-admin at zope.org
Wed May 8 10:11:10 UTC 2013
Log message for revision 130230:
- fixed some search view issues
- split search view in two views which are simpler and more compatible with the old skin
Changed:
U Products.CMFDefault/trunk/Products/CMFDefault/browser/search/configure.zcml
U Products.CMFDefault/trunk/Products/CMFDefault/browser/search/interfaces.py
U Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search.pt
U Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search.py
U Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search_results.pt
U Products.CMFDefault/trunk/Products/CMFDefault/browser/search/tests/test_search.py
U Products.CMFDefault/trunk/Products/CMFDefault/profiles/views_support/actions.xml
U Products.CMFDefault/trunk/Products/CMFDefault/skins/absolut/main_template.pt
-=-
Modified: Products.CMFDefault/trunk/Products/CMFDefault/browser/search/configure.zcml
===================================================================
--- Products.CMFDefault/trunk/Products/CMFDefault/browser/search/configure.zcml 2013-05-08 09:28:41 UTC (rev 130229)
+++ Products.CMFDefault/trunk/Products/CMFDefault/browser/search/configure.zcml 2013-05-08 10:11:09 UTC (rev 130230)
@@ -5,9 +5,18 @@
<browser:page
for="Products.CMFCore.interfaces.ISiteRoot"
layer="Products.CMFDefault.interfaces.ICMFDefaultSkin"
- name="search.html"
+ name="search_form.html"
class=".search.Search"
permission="zope2.View"
/>
+ <browser:page
+ for="Products.CMFCore.interfaces.ISiteRoot"
+ layer="Products.CMFDefault.interfaces.ICMFDefaultSkin"
+ name="search.html"
+ class=".search.SearchView"
+ template="search_results.pt"
+ permission="zope2.View"
+ />
+
</configure>
Modified: Products.CMFDefault/trunk/Products/CMFDefault/browser/search/interfaces.py
===================================================================
--- Products.CMFDefault/trunk/Products/CMFDefault/browser/search/interfaces.py 2013-05-08 09:28:41 UTC (rev 130229)
+++ Products.CMFDefault/trunk/Products/CMFDefault/browser/search/interfaces.py 2013-05-08 10:11:09 UTC (rev 130230)
@@ -38,7 +38,7 @@
def status_vocab(context):
"""Provides a list of workflow states"""
ctool = getUtility(ICatalogTool)
- values = [SimpleTerm(None, None, _(u"-- any --"))]
+ values = [SimpleTerm(u'', '', _(u"-- any --"))]
values += [ SimpleTerm(v, str(v), _(decode(v)))
for v in ctool.uniqueValuesFor('review_state') ]
return SimpleVocabulary(values)
@@ -47,7 +47,7 @@
def subject_vocab(context):
"""Provides a list of subject keywords"""
ctool = getUtility(ICatalogTool)
- values = [SimpleTerm(None, None, _(u"-- any --"))]
+ values = [SimpleTerm(u'', '', _(u"-- any --"))]
values += [SimpleTerm(v, v.encode('hex'), decode(v))
for v in ctool.uniqueValuesFor('Subject')]
return SimpleVocabulary(values)
@@ -56,7 +56,7 @@
def date_vocab(context):
"""Provides a list of dates for searching with"""
mtool = getUtility(IMembershipTool)
- dates = [SimpleTerm(date(1970, 1, 1), date(1970, 1, 1), _(u'Ever'))]
+ dates = [SimpleTerm(None, 'ever', _(u'Ever'))]
if not mtool.isAnonymousUser():
member = mtool.getAuthenticatedMember()
login_time = member.getProperty('last_login_time')
@@ -64,22 +64,22 @@
login_time = DateTime(login_time)
login = date(*login_time.parts()[:3])
dates.append(SimpleTerm(
- login, login, _(u'Last login'))
+ login, 'last_login', _(u'Last login'))
)
today = date.today()
dates.append(SimpleTerm(today - timedelta(days=1),
- today - timedelta(days=1),
+ 'yesterday',
_(u'Yesterday')
)
)
dates.append(SimpleTerm(today - timedelta(days=7),
- today - timedelta(days=7),
+ 'last_week',
_(u'Last week')
)
)
dates.append(SimpleTerm(today - timedelta(days=31),
- today - timedelta(days=31),
+ 'last_month',
_(u'Last month')
)
)
@@ -90,7 +90,7 @@
"""Provides a list of portal types"""
ttool = getUtility(ITypesTool)
types = ttool.listTypeInfo()
- terms = [SimpleTerm(None, None, _(u"-- any --"))]
+ terms = [SimpleTerm(u'', '', _(u"-- any --"))]
terms += [ SimpleTerm(t.getId(), t.getId(), decode(t.Title()))
for t in types ]
return SimpleVocabulary(terms)
@@ -100,77 +100,57 @@
class ISearchSchema(Interface):
review_state = List(
- title=_(u"Review Status"),
- description=_(
- u"As a reviewer, you may search for items based on"
- u" their review state. If you wish to constrain"
- u" results to items in certain states, select them"
- u" from this list."),
- value_type=Choice(
- source=status_vocab
- ),
- required=False,
- )
+ title=_(u'Review Status'),
+ description=_(u"As a reviewer, you may search for items based on "
+ u"their review state. If you wish to constrain results "
+ u"to items in certain states, select them from this "
+ u"list."),
+ value_type=Choice(source=status_vocab),
+ default=[''])
SearchableText = TextLine(
- title=_(u"Full Text"),
- description=_(
- u"For a simple text search, enter your search term"
- u" here. Multiple words may be found by combining them"
- u" with AND and OR. This will find text in items'"
- u" contents, title and description."),
- required=False
- )
+ title=_(u'Full Text'),
+ description=_(u"For a simple text search, enter your search term "
+ u"here. Multiple words may be found by combining them "
+ u"with AND and OR. This will find text in items' "
+ u"contents, title and description."),
+ required=False)
Title = TextLine(
- title=_(u"Title"),
- required=False
- )
+ title=_(u'Title'),
+ required=False)
Subject = List(
- title=_(u"Subject"),
- description=_(u""),
- value_type=Choice(
- source=subject_vocab
- ),
- required=False
- )
+ title=_(u'Subject'),
+ description=_(u""),
+ value_type=Choice(source=subject_vocab),
+ default=[''])
Description = TextLine(
- title=_(u"Description"),
- description=_(
- u"You may also search the items' descriptions and"
- u" titles specifically. Multiple words may be found by"
- u" combining them with AND and OR."),
- required=False
- )
+ title=_(u'Description'),
+ description=_(u"You may also search the items' descriptions and "
+ u"titles specifically. Multiple words may be found by "
+ u"combining them with AND and OR."),
+ required=False)
created = Choice(
- title=_(u"Find new items since..."),
- description=(_(
- u"You may find only recent items by selecting a time-frame."
- )),
- source=date_vocab,
- default=date.today()
- )
+ title=_(u'Find new items since...'),
+ description=_(u"You may find only recent items by selecting a "
+ u"time-frame."),
+ source=date_vocab,
+ required=False)
portal_type = List(
- title=_(u"Item type"),
- description=_(
- u"You may limit your results to particular kinds of"
- u" items by selecting them above. To find all kinds of"
- u" items, do not select anything."),
- value_type=Choice(
- source=type_vocab,
- ),
- required=False,
- )
+ title=_(u'Item type'),
+ description=_(u"You may limit your results to particular kinds of "
+ u"items by selecting them above. To find all kinds of "
+ u"items, do not select anything."),
+ value_type=Choice(source=type_vocab),
+ default=[''])
listCreators = ASCIILine(
- title=_(u"Creator"),
- description=_(
- u"To find items by a particular user only, enter their"
- u" username above. Note that you must enter their username"
- u" exactly."),
- required=False
- )
+ title=_(u'Creator'),
+ description=_(u"To find items by a particular user only, enter their "
+ u"username above. Note that you must enter their "
+ u"username exactly."),
+ required=False)
Modified: Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search.pt
===================================================================
--- Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search.pt 2013-05-08 09:28:41 UTC (rev 130229)
+++ Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search.pt 2013-05-08 10:11:09 UTC (rev 130230)
@@ -1,21 +1,14 @@
<html metal:use-macro="context/@@standard_macros/page">
-
<body>
<metal:slot metal:fill-slot="body" i18n:domain="cmf_default">
+<metal:macro metal:use-macro="context/@@formlib_macros/errors" />
-<ul class="errors" tal:condition="view/errors">
- <li tal:repeat="error view/error_views"
- tal:content="structure error">Error Message</li>
-</ul>
+<metal:macro metal:use-macro="context/@@formlib_macros/header" />
-<h1 tal:content="view/label | nothing ">Portal Search</h1>
+<form action="." method="post"
+ tal:attributes="action request/ACTUAL_URL">
-<form action="." method="post" tal:attributes="action request/ACTUAL_URL">
-
- <tal:block repeat="widget view/hidden_widgets"
- replace="structure widget/hidden" />
-
<fieldset tal:repeat="widget view/widgets">
<label tal:attributes="for widget/name" tal:content="widget/label"></label>
<tal:block replace="structure widget" />
@@ -24,13 +17,12 @@
replace="structure widget/error" />
</fieldset>
- <div class="buttons">
- <tal:loop tal:repeat="action view/search"
- tal:replace="structure action/render" />
- </div>
+<div class="buttons">
+ <tal:loop tal:repeat="action view/actions"
+ tal:replace="structure action/render" />
+</div>
</form>
-
</metal:slot>
</body>
Modified: Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search.py
===================================================================
--- Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search.py 2013-05-08 09:28:41 UTC (rev 130229)
+++ Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search.py 2013-05-08 10:11:09 UTC (rev 130230)
@@ -12,34 +12,31 @@
##############################################################################
"""Search views"""
-import datetime
-
+from DateTime.DateTime import DateTime
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from zope.component import getUtility
from zope.formlib import form
+from ZPublisher.HTTPRequest import record
from .interfaces import ISearchSchema
from Products.CMFCore.interfaces import ICatalogTool
from Products.CMFDefault.browser.utils import memoize
-from Products.CMFDefault.browser.widgets.batch import BatchFormMixin
-from Products.CMFDefault.browser.widgets.batch import IBatchForm
+from Products.CMFDefault.browser.widgets.batch import BatchViewBase
from Products.CMFDefault.formlib.form import EditFormBase
from Products.CMFDefault.formlib.widgets import ChoiceMultiSelectWidget
from Products.CMFDefault.permissions import ReviewPortalContent
from Products.CMFDefault.utils import Message as _
-EPOCH = datetime.date(1970, 1, 1)
+EPOCH = DateTime('1970/01/01 00:00:00 UTC')
-class Search(BatchFormMixin, EditFormBase):
+class Search(EditFormBase):
"""Portal Search Form"""
- template = ViewPageTemplateFile("search.pt")
- results = ViewPageTemplateFile("search_results.pt")
- hidden_fields = form.FormFields(IBatchForm)
+ template = ViewPageTemplateFile('search.pt')
- search = form.Actions(
+ actions = form.Actions(
form.Action(
name='search',
label=_(u"Search"),
@@ -48,24 +45,11 @@
),
)
- # for handling searches from the search box
- image = form.Actions(
- form.Action(
- name='search.x',
- label=_(u"Search"),
- success='handle_search',
- failure='handle_failure',
- ),
- form.Action(
- name='search.y',
- label=_(u"Search"),
- success='handle_search',
- failure='handle_failure',
- ),
- )
+ @property
+ def label(self):
+ return _(u'Search ${portal_title}',
+ mapping={'portal_title': self.title()})
- actions = search + image
-
@property
def form_fields(self):
form_fields = form.FormFields(ISearchSchema)
@@ -76,53 +60,67 @@
form_fields = form_fields.omit('review_state')
return form_fields
- @property
- @memoize
- def catalog(self):
- return getUtility(ICatalogTool)
+ def handle_search(self, action, data):
+ if 'form.created' in self.request.form:
+ del self.request.form['form.created']
+ if 'created' in data and data['created']:
+ created = record()
+ created.query = DateTime(str(data['created']))
+ created.range = 'min'
+ self.request.form['form.created'] = created
+ return self._setRedirect('portal_actions', 'global/search',
+ 'review_state,SearchableText,Title,Subject,'
+ 'Description,created,portal_type,'
+ 'listCreators')
- @memoize
- def _getNavigationVars(self):
- data = {}
- if hasattr(self, 'hidden_widgets'):
- form.getWidgetsData(self.hidden_widgets, self.prefix, data)
- if hasattr(self, '_query'):
- data.update(self._query)
- else:
- data = self.request.form
- return data
- def setUpWidgets(self, ignore_request=False):
- if "form.b_start" in self.request.form \
- or "b_start" in self.request.form:
- self.template = self.results
- super(Search, self).setUpWidgets(ignore_request)
- self.widgets = form.setUpWidgets(
- self.form_fields, self.prefix, self.context,
- self.request, ignore_request=ignore_request)
+class SearchView(BatchViewBase):
- def handle_search(self, action, data):
- for k, v in data.items():
+ """View for search results.
+ """
+
+ # helpers
+
+ @memoize
+ def _getNavigationVars(self):
+ kw = self.request.form.copy()
+ for k, v in kw.items():
if k in ('review_state', 'Title', 'Subject', 'Description',
- 'portal_type', 'listCreators', 'SearchableText'):
- if not v or v == u"None":
- del data[k]
- elif k == 'created' and v == EPOCH:
- del data[k]
- self._query = data
- self.template = self.results
+ 'portal_type', 'listCreators'):
+ if isinstance(v, (list, tuple)):
+ v = filter(None, v)
+ if not v:
+ del kw[k]
+ elif k in ('created',):
+ if v['query'] == EPOCH and v['range'] == 'min':
+ del kw[k]
+ else:
+ # work around problems with DateTime in records
+ kw[k] = v.copy()
+ elif k in ('go', 'go.x', 'go.y'):
+ del kw[k]
+ elif k == 'SearchableText':
+ v = ' '.join([ w.strip('_-.@') for w in v.split() ])
+ if v:
+ kw[k] = v
+ else:
+ del kw[k]
+ return kw
@memoize
def _get_items(self):
- return self.catalog.searchResults(self._query)
+ ctool = getUtility(ICatalogTool)
+ return ctool.searchResults(self._getNavigationVars())
+ # interface
+
@memoize
def listBatchItems(self):
- return({'description': item.Description,
- 'icon': item.getIconURL,
- 'title': item.Title,
- 'type': item.Type,
- 'date': item.Date,
- 'url': item.getURL(),
- 'format': None}
- for item in self._getBatchObj())
+ return ({'description': item.Description,
+ 'icon': item.getIconURL,
+ 'title': item.Title,
+ 'type': item.Type,
+ 'date': item.Date,
+ 'url': item.getURL(),
+ 'format': None}
+ for item in self._getBatchObj())
Modified: Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search_results.pt
===================================================================
--- Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search_results.pt 2013-05-08 09:28:41 UTC (rev 130229)
+++ Products.CMFDefault/trunk/Products/CMFDefault/browser/search/search_results.pt 2013-05-08 10:11:09 UTC (rev 130230)
@@ -1,11 +1,10 @@
<html metal:use-macro="context/@@standard_macros/page">
-
<body>
<metal:slot metal:fill-slot="body" i18n:domain="cmf_default">
+<h1 i18n:translate="">Search Results</h1>
-<h1 tal:content="view/label | nothing ">Search Results</h1>
-
+<metal:macro metal:use-macro="context/@@batch_widget/summary" />
<table class="SearchResults" tal:condition="view/listBatchItems">
<thead>
<tr>
Modified: Products.CMFDefault/trunk/Products/CMFDefault/browser/search/tests/test_search.py
===================================================================
--- Products.CMFDefault/trunk/Products/CMFDefault/browser/search/tests/test_search.py 2013-05-08 09:28:41 UTC (rev 130229)
+++ Products.CMFDefault/trunk/Products/CMFDefault/browser/search/tests/test_search.py 2013-05-08 10:11:09 UTC (rev 130230)
@@ -21,6 +21,7 @@
from zope.i18nmessageid import Message
from zope.testing.cleanup import cleanUp
+from Products.CMFCore.interfaces import IActionsTool
from Products.CMFCore.interfaces import ICatalogTool
from Products.CMFCore.interfaces import IMembershipTool
from Products.CMFCore.interfaces import IPropertiesTool
@@ -59,8 +60,8 @@
self.assertEqual(len(vocab), 3)
terms = [ t for t in vocab ]
- self.assertEqual(terms[0].value, None)
- self.assertEqual(terms[0].token, 'None')
+ self.assertEqual(terms[0].value, u'')
+ self.assertEqual(terms[0].token, '')
self.assertTrue(isinstance(terms[0].token, str))
self.assertEqual(terms[0].title, u'-- any --')
self.assertTrue(isinstance(terms[0].title, Message))
@@ -96,8 +97,8 @@
self.assertEqual(len(vocab), 3)
terms = [ t for t in vocab ]
- self.assertEqual(terms[0].value, None)
- self.assertEqual(terms[0].token, 'None')
+ self.assertEqual(terms[0].value, u'')
+ self.assertEqual(terms[0].token, '')
self.assertTrue(isinstance(terms[0].token, str))
self.assertEqual(terms[0].title, u'-- any --')
self.assertTrue(isinstance(terms[0].title, Message))
@@ -132,29 +133,29 @@
self.assertEqual(len(vocab), 4)
terms = [ t for t in vocab ]
- self.assertEqual(terms[0].value, date(1970, 1, 1))
- self.assertEqual(terms[0].token, '1970-01-01')
+ self.assertEqual(terms[0].value, None)
+ self.assertEqual(terms[0].token, 'ever')
self.assertTrue(isinstance(terms[0].token, str))
self.assertEqual(terms[0].title, u'Ever')
self.assertTrue(isinstance(terms[0].title, Message))
date_value = date.today() - timedelta(days=1)
self.assertEqual(terms[1].value, date_value)
- self.assertEqual(terms[1].token, str(date_value))
+ self.assertEqual(terms[1].token, 'yesterday')
self.assertTrue(isinstance(terms[1].token, str))
self.assertEqual(terms[1].title, u'Yesterday')
self.assertTrue(isinstance(terms[1].title, Message))
date_value = date.today() - timedelta(days=7)
self.assertEqual(terms[2].value, date_value)
- self.assertEqual(terms[2].token, str(date_value))
+ self.assertEqual(terms[2].token, 'last_week')
self.assertTrue(isinstance(terms[2].token, str))
self.assertEqual(terms[2].title, u'Last week')
self.assertTrue(isinstance(terms[2].title, Message))
date_value = date.today() - timedelta(days=31)
self.assertEqual(terms[3].value, date_value)
- self.assertEqual(terms[3].token, str(date_value))
+ self.assertEqual(terms[3].token, 'last_month')
self.assertTrue(isinstance(terms[3].token, str))
self.assertEqual(terms[3].title, u'Last month')
self.assertTrue(isinstance(terms[3].title, Message))
@@ -193,8 +194,8 @@
self.assertEqual(len(vocab), 3)
terms = [ t for t in vocab ]
- self.assertEqual(terms[0].value, None)
- self.assertEqual(terms[0].token, 'None')
+ self.assertEqual(terms[0].value, u'')
+ self.assertEqual(terms[0].token, '')
self.assertTrue(isinstance(terms[0].token, str))
self.assertEqual(terms[0].title, u'-- any --')
self.assertTrue(isinstance(terms[0].title, Message))
@@ -208,41 +209,62 @@
class SearchFormTests(unittest.TestCase):
+ def setUp(self):
+ class DummyActionsTool(object):
+ def getActionInfo(self, action_chain, object=None,
+ check_visibility=False, check_condition=False):
+ return {'url': 'foo'}
+
+ sm = getSiteManager()
+ sm.registerUtility(DummyActionsTool(), IActionsTool)
+
+ def tearDown(self):
+ cleanUp()
+
def _getTargetClass(self):
from Products.CMFDefault.browser.search.search import Search
- return Search(DummySite(), DummyRequest())
+ return Search
+
+ def _makeOne(self):
+ return self._getTargetClass()(DummySite(), DummyRequest())
+
def test_is_not_reviewer(self):
- view = self._getTargetClass()
+ view = self._makeOne()
view._checkPermission = lambda permission: False
self.assertEqual(view.form_fields.get('review_state'), None)
def test_is_reviewer(self):
- view = self._getTargetClass()
+ view = self._makeOne()
view._checkPermission = lambda permission: True
self.assertNotEqual(view.form_fields.get('review_state'), None)
def test_strip_unused_paramaters(self):
- view = self._getTargetClass()
- data = {'portal_type': ['Document'], 'review_state': u"None",
- 'Subject': u"None"}
- view.handle_search('search', data)
- self.assertEqual(view._query, {'portal_type': ['Document']})
+ view = self._makeOne()
+ view.request.form = {'portal_type': ['Document'],
+ 'review_state': u'',
+ 'Subject': u''}
+ view.handle_search('search', {})
+ self.assertEqual(view.request.response.location,
+ 'foo?portal_type:list=Document')
+
+class SearchViewTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from Products.CMFDefault.browser.search.search import SearchView
+
+ return SearchView
+
+ def _makeOne(self):
+ return self._getTargetClass()(DummySite(), DummyRequest())
+
def test_add_search_vars_to_hidden(self):
- view = self._getTargetClass()
- self.assertFalse(hasattr(view, '_query'))
- data = {'portal_type': ['Document']}
- view.handle_search('search', data)
- self.assertEqual(view._getNavigationVars(), data)
+ view = self._makeOne()
+ view.request.form = {'portal_type': ['Document']}
+ self.assertEqual(view._getNavigationVars(), view.request.form)
- def test_search_returns_results(self):
- view = self._getTargetClass()
- self.assertNotEqual(view.template, view.results)
- view.handle_search('search', {})
- self.assertEqual(view.template.filename, view.results.filename)
-
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(StatusVocabularyTests))
@@ -250,4 +272,5 @@
suite.addTest(unittest.makeSuite(DateVocabularyTests))
suite.addTest(unittest.makeSuite(TypeVocabularyTests))
suite.addTest(unittest.makeSuite(SearchFormTests))
+ suite.addTest(unittest.makeSuite(SearchViewTests))
return suite
Modified: Products.CMFDefault/trunk/Products/CMFDefault/profiles/views_support/actions.xml
===================================================================
--- Products.CMFDefault/trunk/Products/CMFDefault/profiles/views_support/actions.xml 2013-05-08 09:28:41 UTC (rev 130229)
+++ Products.CMFDefault/trunk/Products/CMFDefault/profiles/views_support/actions.xml 2013-05-08 10:11:09 UTC (rev 130230)
@@ -34,7 +34,7 @@
<property name="url_expr">string:${portal_url}/@@join.html</property>
</object>
<object name="search_form" meta_type="CMF Action">
- <property name="url_expr">string:${portal_url}/@@search.html</property>
+ <property name="url_expr">string:${portal_url}/@@search_form.html</property>
</object>
<object name="search" meta_type="CMF Action">
<property name="url_expr">string:${portal_url}/@@search.html</property>
Modified: Products.CMFDefault/trunk/Products/CMFDefault/skins/absolut/main_template.pt
===================================================================
--- Products.CMFDefault/trunk/Products/CMFDefault/skins/absolut/main_template.pt 2013-05-08 09:28:41 UTC (rev 130229)
+++ Products.CMFDefault/trunk/Products/CMFDefault/skins/absolut/main_template.pt 2013-05-08 10:11:09 UTC (rev 130230)
@@ -74,9 +74,8 @@
</li>
<form action="search" method="get"
tal:attributes="action globals/search_url">
- <input name="form.SearchableText" type="search" />
- <input type="hidden" name="b_start" value="0" />
- <input type="image" name="form.actions.search" value="go" src="go.gif"
+ <input name="SearchableText" type="search" />
+ <input type="image" name="go" value="go" src="go.gif"
align="middle"
tal:attributes="src string:${portal_url}/go.gif" />
</form>
More information about the checkins
mailing list