source: products/qPloneTabs/branches/quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/plonetabs.py @ 146

Last change on this file since 146 was 146, checked in by mylan, 18 years ago

update uninstall for removing configlet and product's property sheet from portal_properties

  • Property svn:eol-style set to native
File size: 26.8 KB
Line 
1import copy, sys
2from Acquisition import aq_inner
3from OFS.CopySupport import CopyError
4
5from zope.interface import implements
6from zope.component import getUtility, getMultiAdapter
7from zope.i18n import translate
8from zope.schema.interfaces import IVocabularyFactory
9from zope.exceptions import UserError
10from zope.app.container.interfaces import INameChooser
11
12from Products.CMFCore.utils import getToolByName
13from Products.CMFCore.interfaces import IAction, IActionCategory
14from Products.CMFCore.ActionInformation import Action, ActionCategory
15from Products.CMFCore.Expression import Expression
16from Products.CMFPlone import PloneMessageFactory as _
17from Products.CMFPlone import utils
18from Products.CMFPlone.browser.navigation import get_view_url
19from Products.Five.browser import BrowserView
20from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
21from Products.statusmessages.interfaces import IStatusMessage
22
23from plone.app.layout.navigation.root import getNavigationRoot
24from plone.app.kss.plonekssview import PloneKSSView
25from plone.app.workflow.remap import remap_workflow
26from plone.memoize.instance import memoize
27from kss.core import kssaction, KSSExplicitError
28
29from quintagroup.plonetabs.config import *
30from interfaces import IPloneTabsControlPanel
31
32ACTION_ATTRS = ["id", "title", "url_expr", "available_expr", "visible"]
33
34class PloneTabsControlPanel(PloneKSSView):
35   
36    implements(IPloneTabsControlPanel)
37   
38    template = ViewPageTemplateFile("templates/plonetabs.pt")
39    actionslist_template = ViewPageTemplateFile("templates/actionslist.pt")
40    autogenerated_template = ViewPageTemplateFile("templates/autogenerated.pt")
41   
42    # custom templates used to update page sections
43    sections_template = ViewPageTemplateFile("templates/sections.pt")
44   
45    def __call__(self):
46        """ Perform the update and redirect if necessary, or render the page """
47        postback = True
48        errors = {}
49        context = aq_inner(self.context)
50       
51        form = self.request.form
52        action = form.get("action", "")
53        submitted = form.get('form.submitted', False)
54       
55        # action handler def handler(self, form)
56        if submitted:
57            if form.has_key('add.add'):
58                postback = self.manage_addAction(form, errors)
59            elif form.has_key("edit.save"):
60                postback = self.manage_editAction(form, errors)
61            elif form.has_key("edit.delete"):
62                postback = self.manage_deleteAction(form, errors)
63            elif form.has_key("edit.moveup"):
64                postback = self.manage_moveUpAction(form, errors)
65            elif form.has_key("edit.movedown"):
66                postback = self.manage_moveDownAction(form, errors)
67            elif form.has_key("autogenerated.save"):
68                postback = self.manage_setAutogeneration(form, errors)
69            else:
70                postback = True
71       
72        if postback:
73            return self.template(errors=errors)
74   
75    ########################################
76    # Methods for processing configlet posts
77    ########################################
78   
79    def manage_setAutogeneration(self, form, errors):
80        """ Process managing autogeneration settings """
81       
82        # set excludeFromNav property for root objects
83        portal = getMultiAdapter((aq_inner(self.context), self.request), name='plone_portal_state').portal()
84        generated_tabs = form.get("generated_tabs", '0')
85        nonfolderish_tabs = form.get("nonfolderish_tabs", '0')
86       
87        for item in self.getRootTabs():
88            obj = getattr(portal, item['id'], None)
89            if obj is not None:
90                checked = form.get(item['id'], None)
91                if checked == '1':
92                    obj.update(excludeFromNav=False)
93                else:
94                    obj.update(excludeFromNav=True)
95
96        # set disable_folder_sections property
97        if int(generated_tabs) == 1:
98            self.setSiteProperties(disable_folder_sections=False)
99        else:
100            self.setSiteProperties(disable_folder_sections=True)
101       
102        # set disable_nonfolderish_sections property
103        if int(nonfolderish_tabs) == 1:
104            self.setSiteProperties(disable_nonfolderish_sections=False)
105        else:
106            self.setSiteProperties(disable_nonfolderish_sections=True)
107       
108        # after successfull form processing make redirect with good message
109        IStatusMessage(self.request).addStatusMessage(_(u"Changes saved!"), type="info")
110        self.redirect()
111        return False
112   
113    def manage_addAction(self, form, errs):
114        """ Manage method to add a new action to given category,
115            if category doesn't exist, create it """
116        # extract posted data
117        id, cat_name, data = self.parseAddForm(form)
118       
119        # validate posted data
120        errors = self.validateActionFields(cat_name, data)
121       
122        # if not errors find (or create) category and set action to it
123        if not errors:
124            action = self.addAction(cat_name, form)
125            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action successfully added." % action.id), type="info")
126            self.redirect(search="category=%s" % cat_name)
127            return False
128        else:
129            errs.update(errors)
130            IStatusMessage(self.request).addStatusMessage(_(u"Please correct the indicated errors."), type="error")
131            return True
132   
133    def manage_editAction(self, form, errs):
134        """ Manage Method to update action """
135        # extract posted data
136        id, cat_name, data = self.parseEditForm(form)
137       
138        # get category and action to edit
139        category = self.getActionCategory(cat_name)
140        action = category[id]
141       
142        # validate posted data
143        errors = self.validateActionFields(cat_name, data, allow_dup=True)
144       
145        if not errors:
146            action = self.updateAction(id, cat_name, data)
147            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action saved." % action.id), type="info")
148            self.redirect(search="category=%s" % cat_name)
149            return False
150        else:
151            errs.update(self.processErrors(errors, sufix='_%s' % id)) # add edit form sufix to error ids
152            IStatusMessage(self.request).addStatusMessage(_(u"Please correct the indicated errors."), type="error")
153            return True
154   
155    def manage_deleteAction(self, form, errs):
156        """ Manage Method to delete action """
157        # extract posted data
158        id, cat_name, data = self.parseEditForm(form)
159       
160        # get category and action to delete
161        category = self.getActionCategory(cat_name)
162        if id in category.objectIds():
163            self.deleteAction(id, cat_name)
164            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action deleted." % id), type="info")
165            self.redirect(search="category=%s" % cat_name)
166            return False
167        else:
168            IStatusMessage(self.request).addStatusMessage(_(u"No '%s' action in '%s' category." % (id, cat_name)), type="error")
169            return True
170   
171    def manage_moveUpAction(self, form, errs):
172        """ Manage Method for moving up given action by one position """
173        # extract posted data
174        id, cat_name, data = self.parseEditForm(form)
175       
176        # get category and action to move
177        category = self.getActionCategory(cat_name)
178        if id in category.objectIds():
179            self.moveAction(id, cat_name, steps=1)
180            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action moved up." % id), type="info")
181            self.redirect(search="category=%s" % cat_name)
182            return False
183        else:
184            IStatusMessage(self.request).addStatusMessage(_(u"No '%s' action in '%s' category." % (id, cat_name)), type="error")
185            return True
186   
187    def manage_moveDownAction(self, form, errs):
188        """ Manage Method for moving down given action by one position """
189        # extract posted data
190        id, cat_name, data = self.parseEditForm(form)
191       
192        # get category and action to move
193        category = self.getActionCategory(cat_name)
194        if id in category.objectIds():
195            self.moveAction(id, cat_name, steps=-1)
196            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action moved down." % id), type="info")
197            self.redirect(search="category=%s" % cat_name)
198            return False
199        else:
200            IStatusMessage(self.request).addStatusMessage(_(u"No '%s' action in '%s' category." % (id, cat_name)), type="error")
201            return True
202   
203    def redirect(self, url="", search="", hash=""):
204        """ Redirect to @@plonetabs-controlpanel configlet """
205       
206        if url == "":
207            portal_url =  getMultiAdapter((self.context, self.request), name=u"plone_portal_state").portal_url()
208            url = "%s/%s" % (portal_url, "@@plonetabs-controlpanel")
209       
210        if search != "":
211            search = "?%s" % search
212       
213        if hash != "":
214            hash = "#%s" % hash
215       
216        self.request.response.redirect("%s%s%s" % (url, search, hash))
217   
218    ###################################
219    #
220    #  Methods - providers for templates
221    #
222    ###################################
223   
224    def getPageTitle(self, category="portal_tabs"):
225        """ See interface """
226        portal_props = getToolByName(self.context, "portal_properties")
227        default_title = "Plone '%s' Configuration" % category
228       
229        if not hasattr(portal_props, PROPERTY_SHEET):
230            return default_title
231       
232        sheet = getattr(portal_props, PROPERTY_SHEET)
233        if not hasattr(sheet, FIELD_NAME):
234            return default_title
235       
236        field = sheet.getProperty(FIELD_NAME)
237        dict = {}
238        for line in field:
239            cat, title = line.split("|", 2)
240            dict[cat] = title
241       
242        return dict.get(category, None) or default_title
243   
244    def hasActions(self, category="portal_tabs"):
245        """ See interface """
246        return len(getToolByName(self.context, "portal_actions").listActions(categories=[category,])) > 0
247   
248    def getPortalActions(self, category="portal_tabs"):
249        """ See interface """
250        portal_actions = getToolByName(self.context, "portal_actions")
251       
252        if category not in portal_actions.objectIds():
253            return []
254       
255        actions = []
256        for item in portal_actions[category].objectValues():
257            if IAction.providedBy(item):
258                actions.append(item)
259       
260        return actions
261   
262    def isGeneratedTabs(self):
263        """ See interface """
264        site_properties = getToolByName(self.context, "portal_properties").site_properties
265        return not site_properties.getProperty("disable_folder_sections", False)
266   
267    def isNotFoldersGenerated(self):
268        """ See interface """
269        site_properties = getToolByName(self.context, "portal_properties").site_properties
270        return not site_properties.getProperty("disable_nonfolderish_sections", False)
271   
272    def getActionsList(self, category="portal_tabs", errors={}):
273        """ See interface """
274        return self.actionslist_template(category=category, errors=errors)
275   
276    def getGeneratedTabs(self):
277        """ See interface """
278        return self.autogenerated_template()
279   
280    def getRootTabs(self):
281        """ See interface """
282        context = aq_inner(self.context)
283       
284        portal_catalog = getToolByName(context, 'portal_catalog')
285        portal_properties = getToolByName(context, 'portal_properties')
286        navtree_properties = getattr(portal_properties, 'navtree_properties')
287       
288        # Build result dict
289        result = []
290       
291        # check whether we only want actions
292        if not self.isGeneratedTabs():
293            return result
294       
295        query = {}
296       
297        rootPath = getNavigationRoot(context)
298        query['path'] = {'query' : rootPath, 'depth' : 1}
299        query['portal_type'] = utils.typesToList(context)
300       
301        sortAttribute = navtree_properties.getProperty('sortAttribute', None)
302        if sortAttribute is not None:
303            query['sort_on'] = sortAttribute
304           
305            sortOrder = navtree_properties.getProperty('sortOrder', None)
306            if sortOrder is not None:
307                query['sort_order'] = sortOrder
308       
309        if navtree_properties.getProperty('enable_wf_state_filtering', False):
310            query['review_state'] = navtree_properties.getProperty('wf_states_to_show', [])
311       
312        query['is_default_page'] = False
313
314        if not self.isNotFoldersGenerated():
315            query['is_folderish'] = True
316
317        # Get ids not to list and make a dict to make the search fast
318        idsNotToList = navtree_properties.getProperty('idsNotToList', ())
319        excludedIds = {}
320        for id in idsNotToList:
321            excludedIds[id]=1
322
323        rawresult = portal_catalog.searchResults(**query)
324
325        # now add the content to results
326        for item in rawresult:
327            if not excludedIds.has_key(item.getId):
328                id, item_url = get_view_url(item)
329                data = {'name'       : utils.pretty_title_or_id(context, item),
330                        'id'         : id,
331                        'url'        : item_url,
332                        'description': item.Description,
333                        'exclude_from_nav' : item.exclude_from_nav}
334                result.append(data)
335       
336        return result
337   
338    def getCategories(self):
339        """ See interface """
340        portal_actions = getToolByName(self.context, "portal_actions")
341        return portal_actions.objectIds()
342   
343    #
344    # Methods to make this class looks like global sections viewlet
345    #
346   
347    def test(self, condition, ifTrue, ifFalse):
348        """ See interface """
349        if condition:
350            return ifTrue
351        else:
352            return ifFalse
353   
354    # methods for rendering global-sections viewlet via kss,
355    # due to bug in macroContent when global-section list is empty,
356    # ul have condition
357    def portal_tabs(self):
358        """ See global-sections viewlet """
359        actions = context_state = getMultiAdapter((self.context, self.request), name=u"plone_context_state").actions()
360        portal_tabs_view = getMultiAdapter((self.context, self.request), name="portal_tabs_view")
361       
362        return portal_tabs_view.topLevelTabs(actions=actions)
363   
364    def selected_portal_tab(self):
365        """ See global-sections viewlet """
366        selectedTabs = self.context.restrictedTraverse('selectedTabs')
367        selected_tabs = selectedTabs('index_html', self.context, self.portal_tabs())
368       
369        return selected_tabs['portal']
370   
371    ##########################
372    #
373    # KSS Server Actions
374    #
375    ##########################
376   
377    def validateAction(self, id, category, prefix="tabslist_"):
378        """ If action with given id and category doesn't exist - raise kss exception """
379        portal_actions = getToolByName(self.context, "portal_actions")
380       
381        # remove prefix, added for making ids on configlet unique ("tabslist_")
382        act_id = id[len("tabslist_"):]
383       
384        if category not in portal_actions.objectIds():
385            raise KSSExplicitError, "Unexistent root portal actions category %s" % category
386       
387        cat_container = portal_actions[category]
388        if act_id not in map(lambda x: x.id, filter(lambda x: IAction.providedBy(x), cat_container.objectValues())):
389            raise KSSExplicitError, "%s action does not exist in %s category" % (act_id, category)
390       
391        return (cat_container, act_id)
392   
393    @kssaction
394    def toggleGeneratedTabs(self, field, checked='0'):
395        """ Toggle autogenaration setting on configlet """
396       
397        changeProperties = getToolByName(self.context, "portal_properties").site_properties.manage_changeProperties
398        if checked == '1':
399            changeProperties(**{field : False})
400        else:
401            changeProperties(**{field : True})
402       
403        ksscore = self.getCommandSet("core")
404        replace_id = "roottabs"
405        content = self.getGeneratedTabs()
406       
407        ksscore.replaceInnerHTML(ksscore.getHtmlIdSelector(replace_id), content, withKssSetup="True")
408       
409        # update global-sections viewlet
410        self.updatePortalTabs()
411   
412    @kssaction
413    def toggleActionsVisibility(self, id, checked='0', category=None):
414        """ Toggle visibility for portal actions """
415        portal_actions = getToolByName(self.context, "portal_actions")
416        cat_container, act_id = self.validateAction(id, category)
417       
418        if checked == '1':
419            checked = True
420        else:
421            checked = False
422       
423        cat_container[act_id].visible = checked
424       
425        ksscore = self.getCommandSet("core")
426        if checked:
427            ksscore.removeClass(ksscore.getHtmlIdSelector(id), value="invisible")
428        else:
429            ksscore.addClass(ksscore.getHtmlIdSelector(id), value="invisible")
430       
431        self.updatePage(category)
432   
433    @kssaction
434    def toggleRootsVisibility(self, id, checked='0'):
435        """ Toggle visibility for portal root objects (exclude_from_nav) """
436        portal = getMultiAdapter((aq_inner(self.context), self.request), name='plone_portal_state').portal()
437       
438        # remove prefix, added for making ids on configlet unique ("roottabs_")
439        obj_id = id[len("roottabs_"):]
440       
441        if obj_id not in portal.objectIds():
442            raise KSSExplicitError, "Object with %s id doesn't exist in portal root" % obj_id
443       
444        if checked == '1':
445            checked = True
446        else:
447            checked = False
448       
449        portal[obj_id].update(excludeFromNav=not checked)
450       
451        ksscore = self.getCommandSet("core")
452        if checked:
453            ksscore.removeClass(ksscore.getHtmlIdSelector(id), value="invisible")
454        else:
455            ksscore.addClass(ksscore.getHtmlIdSelector(id), value="invisible")
456       
457        # update global-sections viewlet
458        self.updatePortalTabs()
459   
460    @kssaction
461    def kss_deleteAction(self, id, category):
462        """ Delete portal action with given id & category """
463        portal_actions = getToolByName(self.context, "portal_actions")
464        cat_container, act_id = self.validateAction(id, category)
465       
466        cat_container.manage_delObjects(ids=[act_id,])
467       
468        # update action list on client
469        ksscore = self.getCommandSet("core")
470       
471        ksscore.deleteNode(ksscore.getHtmlIdSelector(id))
472       
473        # add "noitems" class to Reorder controls to hide it
474        if not filter(lambda x: IAction.providedBy(x), cat_container.objectValues()):
475            ksscore.addClass(ksscore.getHtmlIdSelector("reorder"), value="noitems")
476       
477        # XXX TODO: fade effect during removing, for this kukit js action/command plugin needed
478       
479        self.updatePage(category)
480   
481    @kssaction
482    def oldAddAction(self, id, name, action='', category='portal_tabs', condition='', visible=False):
483        pass
484   
485    @kssaction
486    def editAction(self, id, category):
487        """ Show edit form for given action """
488        cat_container, act_id = self.validateAction(id, category)
489       
490        # collect data
491        action_info = self.copyAction(cat_container[act_id])
492        action_info["editing"] = True
493       
494        ksscore = self.getCommandSet("core")
495        content = self.actionslist_template(tabs=[action_info,])
496        replace_id = id
497       
498        ksscore.replaceHTML(ksscore.getHtmlIdSelector(replace_id), content)
499       
500        # focus name field
501        ksscore.focus(ksscore.getCssSelector("#%s input[name=name_%s]" % (id, act_id)))
502   
503    @kssaction
504    def editCancel(self, id, category):
505        """ Hide edit form for given action """
506        cat_container, act_id = self.validateAction(id, category)
507       
508        ksscore = self.getCommandSet("core")
509        content = self.actionslist_template(tabs=[cat_container[act_id],])
510        replace_id = id
511       
512        ksscore.replaceHTML(ksscore.getHtmlIdSelector(replace_id), content)
513   
514    #
515    # Utility Methods
516    #
517   
518    def copyAction(self, action):
519        """ Copyt action to dictionary """
520        action_info = {}
521        for attr in ACTION_ATTRS:
522            action_info[attr] = getattr(action, attr)
523        return action_info
524   
525    def validateActionFields(self, cat_name, data, allow_dup=False):
526        """ Check action fields on validity """
527        errors = {}
528       
529        if allow_dup:
530            category = ActionCategory(cat_name)           # create dummy category to avoid id duplication during action update
531        else:
532            category = self.getOrCreateCategory(cat_name) # get or create (if necessary) actions category
533       
534        # validate action id
535        chooser = INameChooser(category)
536        try:
537            chooser.checkName(data['id'], self.context)
538        except Exception, e:
539            errors['id'] = "%s" % str(e)
540       
541        # validate action name
542        if not data['title'].strip():
543            errors['title'] = 'Empty or invalid title specified'
544       
545        # validate condition expression
546        if data['available_expr']:
547            try:
548                Expression(data['available_expr'])
549            except Exception, e:
550                errors["available_expr"] = "%s" % str(e)
551       
552        # validate action expression
553        if data['url_expr']:
554            try:
555                Expression(data['url_expr'])
556            except Exception, e:
557                errors["url_expr"] = "%s" % str(e)
558       
559        return errors
560   
561    def processErrors(self, errors, prefix='', sufix=''):
562        """ Add prefixes, sufixes to error ids
563            This is necessary during edit form validation,
564            because every edit form on the page has it's own sufix (id) """
565        if not (prefix or sufix):
566            return errors
567       
568        result = {}
569        for key, value in errors.items():
570            result['%s%s%s' % (prefix, key, sufix)] = value
571       
572        return result
573   
574    def parseEditForm(self, form):
575        """ Extract all needed fields from edit form """
576        # get original id and category
577        info = {}
578        id = form['orig_id']
579        category = form['category']
580       
581        # preprocess 'visible' field (checkbox needs special checking)
582        if form.has_key('visible_%s' % id):
583            form['visible_%s' % id] = True
584        else:
585            form['visible_%s' % id] = False
586       
587        # collect all action fields
588        for attr in ACTION_ATTRS:
589            info[attr] = form['%s_%s' % (attr, id)]
590       
591        return (id, category, info)
592   
593    def parseAddForm(self, form):
594        """ Extract all needed fields from add form """
595        info = {}
596        id = form['id']
597        category = form['category']
598       
599        # preprocess 'visible' field (checkbox needs special checking)
600        if form.has_key('visible'):
601            form['visible'] = True
602        else:
603            form['visible'] = False
604       
605        # collect all action fields
606        for attr in ACTION_ATTRS:
607            info[attr] = form[attr]
608       
609        return (id, category, info)
610   
611    def getActionCategory(self, name):
612        portal_actions = getToolByName(self.context, 'portal_actions')
613        return portal_actions[name]
614   
615    def getOrCreateCategory(self, name):
616        """ Get or create (if necessary) category """
617        portal_actions = getToolByName(self.context, 'portal_actions')
618        if name not in map(lambda x: x.id, filter(lambda x: IActionCategory.providedBy(x), portal_actions.objectValues())):
619            portal_actions._setObject(name, ActionCategory(name))
620        return self.getActionCategory(name)
621   
622    def addAction(self, cat_name, data):
623        """ Create and add new action to category with given name """
624        id = data.pop('id')
625        category = self.getOrCreateCategory(cat_name)
626        action = Action(id, **data)
627        category._setObject(id, action)
628        return action
629   
630    def updateAction(self, id, cat_name, data):
631        """ Update action with given id and category """
632        new_id = data.pop('id')
633        category = self.getActionCategory(cat_name)
634       
635        # rename action if necessary
636        if id != new_id:
637            category.manage_renameObject(id, new_id)
638       
639        # get action
640        action = category[new_id]
641       
642        # update action properties
643        for attr in ACTION_ATTRS:
644            if data.has_key(attr):
645                action._setPropValue(attr, data[attr])
646       
647        return action
648   
649    def deleteAction(self, id, cat_name):
650        """ Delete action with given id from given category """
651        category = self.getActionCategory(cat_name)
652        category.manage_delObjects(ids=[id,])
653        return True
654   
655    def moveAction(self, id, cat_name, steps=0):
656        """ Move action by a given steps """
657        if steps != 0:
658            category = self.getActionCategory(cat_name)
659            if steps > 0:
660                category.moveObjectsUp([id,], steps)
661            else:
662                category.moveObjectsDown([id,], abs(steps))
663            return True
664        return False
665   
666    def setSiteProperties(self, **kw):
667        """ Change site_properties """
668        site_properties = getToolByName(self.context, "portal_properties").site_properties
669        site_properties.manage_changeProperties(**kw)
670        return True
671   
672    #
673    # KSS Methods that are used to update different parts of the page
674    # according to category
675    #
676   
677    def updatePage(self, category):
678        """ Seek for according method in class and calls it if found
679            Example of making up method's name:
680                portal_tabs => updatePortalTabs """
681        method_name = 'update%sPageSection' % ''.join(map(lambda x: x.capitalize(), category.split('_')))
682        if hasattr(self, method_name):
683            getattr(self, method_name)()
684   
685    def updatePortalTabsPageSection(self):
686        """ Method for updating global-sections on client """
687        ksscore = self.getCommandSet("core")
688        ksscore.replaceHTML(
689            ksscore.getHtmlIdSelector("portal-globalnav"),
690            self.sections_template(),
691            withKssSetup="False")
692   
693    def updateSiteActionsPageSection(self):
694        """ Method for updating site action on client """
695        ksszope = self.getCommandSet("zope")
696        ksszope.refreshViewlet(
697            self.getCommandSet("core").getHtmlIdSelector("portal-siteactions"),
698            "plone.portalheader",
699            "plone.site_actions")
700   
701    def updateUserPageSection(self):
702        """ Method for updating site action on client """
703        ksszope = self.getCommandSet("zope")
704        ksszope.refreshViewlet(
705            self.getCommandSet("core").getHtmlIdSelector("portal-personaltools-wrapper"),
706            "plone.portaltop",
707            "plone.personal_bar")
708
709
Note: See TracBrowser for help on using the repository browser.