source: products/quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/plonetabs.py @ 3563

Last change on this file since 3563 was 3563, checked in by potar, 12 years ago

added compatibility with Plone 4.3; added utils for work with viewlets

  • Property svn:eol-style set to native
File size: 45.5 KB
Line 
1import urllib
2import re
3
4from Acquisition import aq_inner
5from DateTime import DateTime
6
7from zope.interface import implements
8from zope.component import getMultiAdapter
9
10# BBB: compatibility with older plone versions
11try:
12     # Plone < 4.3
13     from zope.app.container import interfaces
14     INameChooser = interfaces.INameChooser
15except ImportError:
16     # Plone >= 4.3
17     from zope.container.interfaces import INameChooser
18     
19from zope.viewlet.interfaces import IViewletManager, IViewlet
20
21from plone.app.layout.navigation.root import getNavigationRoot
22from plone.app.kss.plonekssview import PloneKSSView
23from kss.core import kssaction, KSSExplicitError
24
25from Products.CMFCore.utils import getToolByName
26from Products.CMFCore.interfaces import IAction, IActionCategory
27from Products.CMFCore.ActionInformation import Action, ActionCategory
28from Products.CMFCore.Expression import Expression
29from Products.CMFPlone import utils
30from Products.CMFPlone.browser.navigation import get_view_url
31from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
32from Products.Five.browser import BrowserView
33from Products.statusmessages.interfaces import IStatusMessage
34
35from quintagroup.plonetabs.config import PROPERTY_SHEET, FIELD_NAME
36from quintagroup.plonetabs.utils import setupViewletByName
37from quintagroup.plonetabs import messageFactory as _
38from interfaces import IPloneTabsControlPanel
39
40ACTION_ATTRS = ["id", "title", "url_expr", "available_expr", "visible"]
41UI_ATTRS = {"id": "id",
42            "title": "name",
43            "url_expr": "action",
44            "available_expr": "condition",
45            "visible": "visible"}
46
47bad_id = re.compile(r'[^a-zA-Z0-9-_~,.$\(\)# @]').search
48
49cookie_name = 'ploneTabsMode'
50
51
52class PloneTabsControlPanel(PloneKSSView):
53
54    implements(IPloneTabsControlPanel)
55
56    template = ViewPageTemplateFile("templates/plonetabs.pt")
57    actionslist_template = ViewPageTemplateFile("templates/actionslist.pt")
58    autogenerated_template = ViewPageTemplateFile("templates/autogenerated.pt")
59    autogenerated_list = ViewPageTemplateFile("templates/autogeneratedlist.pt")
60    link_template = ViewPageTemplateFile("templates/changemodelink.pt")
61
62    # custom templates used to update page sections
63    sections_template = ViewPageTemplateFile("templates/sections.pt")
64
65    # configuration variables
66    prefix = "tabslist_"
67    sufix = ""
68
69    def __call__(self):
70        """Perform the update and redirect if necessary, or render the page"""
71        postback = True
72        errors = {}
73
74        form = self.request.form
75        submitted = form.get('form.submitted', False)
76
77        # action handler def handler(self, form)
78        if submitted:
79            if form.has_key('add.add'):
80                postback = self.manage_addAction(form, errors)
81            elif form.has_key("edit.save"):
82                postback = self.manage_editAction(form, errors)
83            elif form.has_key("edit.delete"):
84                postback = self.manage_deleteAction(form, errors)
85            elif form.has_key("edit.moveup"):
86                postback = self.manage_moveUpAction(form, errors)
87            elif form.has_key("edit.movedown"):
88                postback = self.manage_moveDownAction(form, errors)
89            elif form.has_key("autogenerated.save"):
90                postback = self.manage_setAutogeneration(form, errors)
91            else:
92                postback = True
93
94        mode = self.request.get(cookie_name, False)
95        if mode in ('plain', 'rich'):
96            # set cookie to remember the choice
97            expires = (DateTime() + 365).toZone('GMT').rfc822()
98            self.request.response.setCookie(cookie_name, mode, path='/',
99                                            expires=expires)
100
101        if postback:
102            return self.template(errors=errors)
103
104    ########################################
105    # Methods for processing configlet posts
106    ########################################
107
108    def manage_setAutogeneration(self, form, errors):
109        """Process managing autogeneration settings"""
110
111        # set excludeFromNav property for root objects
112        portal = getMultiAdapter((aq_inner(self.context), self.request),
113                                 name='plone_portal_state').portal()
114        generated_tabs = form.get("generated_tabs", '0')
115        nonfolderish_tabs = form.get("nonfolderish_tabs", '0')
116
117        for item in self.getRootTabs():
118            obj = getattr(portal, item['id'], None)
119            if obj is not None:
120                checked = form.get(item['id'], None)
121                if checked == '1':
122                    obj.update(excludeFromNav=False)
123                else:
124                    obj.update(excludeFromNav=True)
125
126        # set disable_folder_sections property
127        if int(generated_tabs) == 1:
128            self.setSiteProperties(disable_folder_sections=False)
129        else:
130            self.setSiteProperties(disable_folder_sections=True)
131
132        # set disable_nonfolderish_sections property
133        if int(nonfolderish_tabs) == 1:
134            self.setSiteProperties(disable_nonfolderish_sections=False)
135        else:
136            self.setSiteProperties(disable_nonfolderish_sections=True)
137
138        # after successfull form processing make redirect with good message
139        IStatusMessage(self.request).addStatusMessage(_(u"Changes saved!"),
140                                                      type="info")
141        self.redirect()
142        return False
143
144    def manage_addAction(self, form, errs):
145        """Manage method to add a new action to given category,
146        if category doesn't exist, create it
147        """
148        # extract posted data
149        id, cat_name, data = self.parseAddForm(form)
150
151        # validate posted data
152        errors = self.validateActionFields(cat_name, data)
153
154        # if not errors find (or create) category and set action to it
155        if not errors:
156            action = self.addAction(cat_name, data)
157            IStatusMessage(self.request).addStatusMessage(
158                _(u"'${id}' action successfully added.",
159                  mapping={'id': action.id}), type="info")
160            self.redirect(search="category=%s" % cat_name)
161            return False
162        else:
163            errs.update(errors)
164            IStatusMessage(self.request).addStatusMessage(
165                _(u"Please correct the indicated errors."), type="error")
166            return True
167
168    def manage_editAction(self, form, errs):
169        """Manage Method to update action"""
170        # extract posted data
171        id, cat_name, data = self.parseEditForm(form)
172
173        # get category and action to edit
174        category = self.getActionCategory(cat_name)
175        action = category[id]
176
177        # validate posted data
178        errors = self.validateActionFields(cat_name, data,
179            allow_dup=(id == data['id']))
180
181        if not errors:
182            action = self.updateAction(id, cat_name, data)
183            IStatusMessage(self.request).addStatusMessage(
184                _(u"'${id}' action saved.", mapping={'id': action.id}),
185                type="info")
186            self.redirect(search="category=%s" % cat_name)
187            return False
188        else:
189            errs.update(self.processErrors(errors,
190                sufix='_%s' % id))  # add edit form sufix to error ids
191            IStatusMessage(self.request).addStatusMessage(
192                _(u"Please correct the indicated errors."), type="error")
193            return True
194
195    def manage_deleteAction(self, form, errs):
196        """Manage Method to delete action"""
197        # extract posted data
198        id, cat_name, data = self.parseEditForm(form)
199
200        # get category and action to delete
201        category = self.getActionCategory(cat_name)
202        if id in category.objectIds():
203            self.deleteAction(id, cat_name)
204            IStatusMessage(self.request).addStatusMessage(
205                _(u"'${id}' action deleted.", mapping={'id': id}), type="info")
206            self.redirect(search="category=%s" % cat_name)
207            return False
208        else:
209            IStatusMessage(self.request).addStatusMessage(
210                _(u"No '${id}' action in '${cat_name}' category.",
211                  mapping={'id': id, 'cat_name': cat_name}),
212                type="error")
213            return True
214
215    def manage_moveUpAction(self, form, errs):
216        """Manage Method for moving up given action by one position"""
217        # extract posted data
218        id, cat_name, data = self.parseEditForm(form)
219
220        # get category and action to move
221        category = self.getActionCategory(cat_name)
222        if id in category.objectIds():
223            self.moveAction(id, cat_name, steps=1)
224            IStatusMessage(self.request).addStatusMessage(
225               _(u"'${id}' action moved up.", mapping={'id': id}), type="info")
226            self.redirect(search="category=%s" % cat_name)
227            return False
228        else:
229            IStatusMessage(self.request).addStatusMessage(
230                _(u"No '${id}' action in '${cat_name}' category.",
231                  mapping={'id': id, 'cat_name': cat_name}), type="error")
232            return True
233
234    def manage_moveDownAction(self, form, errs):
235        """Manage Method for moving down given action by one position"""
236        # extract posted data
237        id, cat_name, data = self.parseEditForm(form)
238
239        # get category and action to move
240        category = self.getActionCategory(cat_name)
241        if id in category.objectIds():
242            self.moveAction(id, cat_name, steps=-1)
243            IStatusMessage(self.request).addStatusMessage(
244                _(u"'${id}' action moved down.", mapping={'id': id}),
245                type="info")
246            self.redirect(search="category=%s" % cat_name)
247            return False
248        else:
249            IStatusMessage(self.request).addStatusMessage(
250                _(u"No '${id}' action in '${cat_name}' category.",
251                  mapping={'id': id, 'cat_name': cat_name}),
252                type="error")
253            return True
254
255    def redirect(self, url="", search="", url_hash=""):
256        """Redirect to @@plonetabs-controlpanel configlet"""
257        if not url:
258            portal_url = getMultiAdapter((self.context, self.request),
259                name=u"plone_portal_state").portal_url()
260            url = '%s/%s' % (portal_url, "@@plonetabs-controlpanel")
261        if search:
262            search = '?%s' % search
263        if url_hash:
264            url_hash = '#%s' % url_hash
265        self.request.response.redirect("%s%s%s" % (url, search, url_hash))
266
267    ###################################
268    #
269    #  Methods - providers for templates
270    #
271    ###################################
272
273    def _charset(self):
274        pp = getToolByName(self.context, 'portal_properties', None)
275        if pp is not None:
276            site_properties = getattr(pp, 'site_properties', None)
277            if site_properties is not None:
278                return site_properties.getProperty('default_charset', 'utf-8')
279        return 'utf-8'
280
281    def getPageTitle(self, category="portal_tabs"):
282        """See interface"""
283        portal_props = getToolByName(self.context, "portal_properties")
284        default_title = _(u"Plone '${cat_name}' Configuration",
285                          mapping={'cat_name': category})
286
287        if not hasattr(portal_props, PROPERTY_SHEET):
288            return default_title
289
290        sheet = getattr(portal_props, PROPERTY_SHEET)
291        if not hasattr(sheet, FIELD_NAME):
292            return default_title
293
294        field = sheet.getProperty(FIELD_NAME)
295        dict = {}
296        for line in field:
297            cat, title = line.split("|", 2)
298            dict[cat] = title
299
300        title = dict.get(category, None)
301        if title is None:
302            return default_title
303
304        charset = self._charset()
305        if not isinstance(title, unicode):
306            title = title.decode(charset)
307
308        return _(title)
309
310    def hasActions(self, category="portal_tabs"):
311        """See interface"""
312        tool = getToolByName(self.context, "portal_actions")
313        return len(tool.listActions(categories=[category, ])) > 0
314
315    def getPortalActions(self, category="portal_tabs"):
316        """See interface"""
317        portal_actions = getToolByName(self.context, "portal_actions")
318
319        if category not in portal_actions.objectIds():
320            return []
321
322        actions = []
323        for item in portal_actions[category].objectValues():
324            if IAction.providedBy(item):
325                actions.append(item)
326
327        return actions
328
329    def isGeneratedTabs(self):
330        """See interface"""
331        site_properties = getToolByName(self.context,
332                                        "portal_properties").site_properties
333        return not site_properties.getProperty("disable_folder_sections",
334                                               False)
335
336    def isNotFoldersGenerated(self):
337        """See interface"""
338        site_properties = getToolByName(self.context,
339                                        "portal_properties").site_properties
340        prop = site_properties.getProperty("disable_nonfolderish_sections",
341                                           False)
342        return not prop
343
344    def getActionsList(self, category="portal_tabs", errors={}, tabs=[]):
345        """See interface"""
346        kw = {'category': category, 'errors': errors}
347        if tabs:
348            kw['tabs'] = tabs
349        return self.actionslist_template(**kw)
350
351    def getAutoGenereatedSection(self, cat_name, errors={}):
352        """See interface"""
353        return self.autogenerated_template(category=cat_name, errors=errors)
354
355    def getGeneratedTabs(self):
356        """See interface"""
357        return self.autogenerated_list()
358
359    def getRootTabs(self):
360        """See interface"""
361        context = aq_inner(self.context)
362
363        portal_catalog = getToolByName(context, 'portal_catalog')
364        portal_properties = getToolByName(context, 'portal_properties')
365        navtree_properties = getattr(portal_properties, 'navtree_properties')
366
367        # Build result dict
368        result = []
369
370        # check whether tabs autogeneration is turned on
371        if not self.isGeneratedTabs():
372            return result
373
374        query = {}
375        rootPath = getNavigationRoot(context)
376        query['path'] = {'query': rootPath, 'depth': 1}
377        query['portal_type'] = utils.typesToList(context)
378
379        sortAttribute = navtree_properties.getProperty('sortAttribute', None)
380        if sortAttribute is not None:
381            query['sort_on'] = sortAttribute
382
383            sortOrder = navtree_properties.getProperty('sortOrder', None)
384            if sortOrder is not None:
385                query['sort_order'] = sortOrder
386
387        if navtree_properties.getProperty('enable_wf_state_filtering', False):
388            query['review_state'] = navtree_properties.getProperty(
389                'wf_states_to_show', [])
390
391        query['is_default_page'] = False
392
393        if not self.isNotFoldersGenerated():
394            query['is_folderish'] = True
395
396        # Get ids not to list and make a dict to make the search fast
397        idsNotToList = navtree_properties.getProperty('idsNotToList', ())
398        excludedIds = {}
399        for id in idsNotToList:
400            excludedIds[id] = 1
401
402        rawresult = portal_catalog.searchResults(**query)
403
404        # now add the content to results
405        for item in rawresult:
406            if not excludedIds.has_key(item.getId):
407                id, item_url = get_view_url(item)
408                data = {'name': utils.pretty_title_or_id(context, item),
409                        'id': id,
410                        'url': item_url,
411                        'description': item.Description,
412                        'exclude_from_nav': item.exclude_from_nav}
413                result.append(data)
414
415        return result
416
417    def getCategories(self):
418        """See interface"""
419        portal_actions = getToolByName(self.context, "portal_actions")
420        return portal_actions.objectIds()
421
422    #
423    # Methods to make this class looks like global sections viewlet
424    #
425
426    def test(self, condition, ifTrue, ifFalse):
427        """See interface"""
428        if condition:
429            return ifTrue
430        else:
431            return ifFalse
432
433    # methods for rendering global-sections viewlet via kss,
434    # due to bug in macroContent when global-section list is empty,
435    # ul have condition
436    def portal_tabs(self):
437        """See global-sections viewlet"""
438        actions = getMultiAdapter((self.context, self.request),
439                                   name=u'plone_context_state').actions()
440        actions_tabs = []
441        try:
442            # Plone 4 and higher
443            import plone.app.upgrade
444            plone.app.upgrade  # pyflakes
445        except ImportError:
446            actions_tabs = actions
447        if not actions_tabs and 'portal_tabs' in actions:
448            actions_tabs = actions['portal_tabs']
449
450        portal_tabs_view = getMultiAdapter((self.context, self.request),
451            name="portal_tabs_view")
452        return portal_tabs_view.topLevelTabs(actions=actions_tabs)
453
454    def selected_portal_tab(self):
455        """See global-sections viewlet"""
456        # BBB: compatibility with older plone versions.
457        # ``selectedTabs`` Python script was merged into the
458        # GlobalSectionsViewlet.
459        section_viewlet = setupViewletByName(self,
460                                             self.context,
461                                             self.request,
462                                             'plone.global_sections')
463        if section_viewlet:
464            # Plone >= 4.3
465            selected_tabs = section_viewlet.selectedTabs(
466                default_tab='index_html', 
467                portal_tabs=self.portal_tabs())
468        else:
469            # Plone < 4.3
470            selectedTabs = self.context.restrictedTraverse('selectedTabs')
471            selected_tabs = selectedTabs('index_html', self.context,
472                self.portal_tabs())
473
474        return selected_tabs['portal']
475
476    ##########################
477    #
478    # KSS Server Actions
479    #
480    ##########################
481
482    @kssaction
483    def kss_insertModeLink(self):
484        """Insert link which allows change categories between plain and rich"""
485        ksscore = self.getCommandSet('core')
486
487        html = self.link_template()
488        target = ksscore.getCssSelector('.link-parent')
489        ksscore.insertHTMLAfter(target, html, withKssSetup='False')
490
491    @kssaction
492    def kss_changeCategory(self, cat_name):
493        """Change action category to manage"""
494        ksscore = self.getCommandSet('core')
495
496        # update actions list
497        actionslist = self.getActionsList(category=cat_name)
498        ksscore.replaceInnerHTML(ksscore.getHtmlIdSelector('tabslist'),
499            actionslist)
500
501        # update autogenerated sections
502        section = self.getAutoGenereatedSection(cat_name)
503        ksscore.replaceHTML(
504            ksscore.getHtmlIdSelector('autogeneration_section'), section)
505        # and title
506        ts = getToolByName(self.context, 'translation_service')
507        title = ts.translate(self.getPageTitle(cat_name), context=self.context)
508        ksscore.replaceInnerHTML(
509            ksscore.getHtmlIdSelector('plonetabs-form-title'), title)
510
511        # update category hidden field on adding form
512        ksscore.setAttribute(ksscore.getCssSelector(
513            'form[name=addaction_form] input[name=category]'),
514            'value',
515            cat_name)
516
517        # update state variable 'plonetabs-category' on client
518        ksscore.setStateVar('plonetabs-category', cat_name)
519
520        # hide adding form
521        self.kss_hideAddForm()
522
523        # issue portal status message
524        self.kss_issueMessage(_(u"Category changed successfully."))
525
526    @kssaction
527    def kss_toggleGeneratedTabs(self, field, checked='0'):
528        """Toggle autogenaration setting on configlet"""
529        if checked == '1':
530            self.setSiteProperties(**{field: False})
531            if field == 'disable_nonfolderish_sections':
532                message = _(u"Generated not folderish tabs switched on.")
533            else:
534                message = _(u"Generated tabs switched on.")
535        else:
536            self.setSiteProperties(**{field: True})
537            if field == 'disable_nonfolderish_sections':
538                message = _(u"Generated not folderish tabs switched off.")
539            else:
540                message = _(u"Generated tabs switched off.")
541
542        # update client
543        ksscore = self.getCommandSet("core")
544        content = self.getGeneratedTabs()
545        ksscore.replaceInnerHTML(ksscore.getHtmlIdSelector('roottabs'),
546                                 content)
547
548        # update global-sections viewlet
549        self.updatePortalTabsPageSection()
550
551        # issue portal status message
552        self.kss_issueMessage(message)
553
554    @kssaction
555    def kss_toggleRootsVisibility(self, id, checked='0'):
556        """Toggle visibility for portal root objects (exclude_from_nav)"""
557        portal = getMultiAdapter((aq_inner(self.context), self.request),
558            name='plone_portal_state').portal()
559
560        # remove prefix, added for making ids on configlet unique ("roottabs_")
561        obj_id = id[len("roottabs_"):]
562
563        if obj_id not in portal.objectIds():
564            raise KSSExplicitError("Object with %s id doesn't" +\
565                                   " exist in portal root." % obj_id)
566
567        if checked == '1':
568            checked = True
569        else:
570            checked = False
571
572        portal[obj_id].update(excludeFromNav=not checked)
573
574        # update client
575        ksscore = self.getCommandSet("core")
576        if checked:
577            ksscore.removeClass(ksscore.getHtmlIdSelector(id),
578                value="invisible")
579            message = _(u"'${id}' object was included into navigation.",
580                        mapping={'id': obj_id})
581        else:
582            ksscore.addClass(ksscore.getHtmlIdSelector(id), value="invisible")
583            message = _(u"'${id}' object was excluded from navigation.",
584                        mapping={'id': obj_id})
585
586        # update global-sections viewlet
587        self.updatePortalTabsPageSection()
588
589        # issue portal status message
590        self.kss_issueMessage(message)
591
592    @kssaction
593    def kss_toggleActionsVisibility(self, id, checked='0', cat_name=None):
594        """Toggle visibility for portal actions"""
595        # validate input
596        act_id, category, action = self.kss_validateAction(id, cat_name)
597        self.updateAction(act_id, cat_name,
598            {'id': act_id, 'visible': (checked == '1') or False})
599
600        # update client
601        ksscore = self.getCommandSet("core")
602        if checked == '1':
603            ksscore.removeClass(ksscore.getHtmlIdSelector(id),
604                value="invisible")
605            message = _(u"'${id}' action is now visible.",
606                        mapping={'id': act_id})
607        else:
608            ksscore.addClass(ksscore.getHtmlIdSelector(id), value="invisible")
609            message = _(u"'${id}' action is now invisible.",
610                        mapping={'id': act_id})
611        self.updatePage(cat_name)
612
613        # issue portal status message
614        self.kss_issueMessage(message)
615
616    @kssaction
617    def kss_deleteAction(self, id, cat_name):
618        """Delete portal action with given id & category"""
619        # validate input
620        act_id, category, action = self.kss_validateAction(id, cat_name)
621        self.deleteAction(act_id, cat_name)
622
623        # update client
624        ksscore = self.getCommandSet("core")
625        # XXX TODO: fade effect during removing, to do this
626        # we need kukit js action/command plugin
627        ksscore.deleteNode(ksscore.getHtmlIdSelector(id))
628
629        # update different sections of page depending on actions category
630        self.updatePage(cat_name)
631
632        # issue portal status message
633        self.kss_issueMessage(_(u"'${id}' action successfully removed.",
634                                mapping={'id': act_id}))
635
636    @kssaction
637    def kss_addAction(self, cat_name):
638        """KSS method to add new portal action"""
639        # extract posted data
640        id, ie7bad_category, data = self.parseAddForm(self.request.form)
641
642        # validate posted data
643        errors = self.validateActionFields(cat_name, data)
644
645        # if not errors find (or create) category and set action to it
646        ksscore = self.getCommandSet('core')
647        if not errors:
648            action = self.addAction(cat_name, data)
649
650            # update client
651            # add one more action to actions list
652            content = self.getActionsList(category=cat_name, tabs=[action, ])
653            ksscore.insertHTMLAsLastChild(
654                                         ksscore.getHtmlIdSelector('tabslist'),
655                                         content)
656
657            # update reorder controls
658            #self.kss_checkReorderControls(cat_name)
659
660            # hide adding form
661            ksscore.removeClass(ksscore.getHtmlIdSelector('addaction'),
662                'adding')
663            self.kss_toggleCollapsible(
664                ksscore.getCssSelector('form[name=addaction_form] '
665                                       '.headerAdvanced'), collapse='true')
666
667            # set client state var 'plonetabs-addingTitle' to empty
668            # string for correct id autogeneration functionality
669            ksscore.setStateVar('plonetabs-addingTitle', '')
670
671            # reset adding form
672            self.kss_resetForm(ksscore.getHtmlIdSelector('addaction'))
673
674            # remove focus from name input
675            self.kss_blur(ksscore.getHtmlIdSelector('actname'))
676
677            message = _(u"'${id}' action successfully added.",
678                        mapping={'id': action.id})
679            msgtype = "info"
680
681            # update page
682            self.updatePage(cat_name)
683        else:
684            # expand advanced section if there are errors in id or condition
685            if errors.has_key('id') or errors.has_key('available_expr'):
686                self.kss_toggleCollapsible(
687                   ksscore.getCssSelector('form[name=addaction_form] '
688                                          '.headerAdvanced'), collapse='false')
689
690            message = _(u"Please correct the indicated errors.")
691            msgtype = "error"
692
693        # update errors on client form
694        self.kss_issueErrors(errors)
695
696        # issue portal status message
697        self.kss_issueMessage(message, msgtype)
698
699    @kssaction
700    def kss_hideAddForm(self):
701        """"Hide Add form, reset it and remove error messages"""
702        # no server changes, only update client
703        ksscore = self.getCommandSet("core")
704
705        # hide form itself
706        ksscore.removeClass(ksscore.getHtmlIdSelector('addaction'), 'adding')
707
708        # collapse advanced section
709        self.kss_toggleCollapsible(
710           ksscore.getCssSelector('form[name=addaction_form] .headerAdvanced'),
711                                  collapse='true')
712
713        # reset form inputs
714        self.kss_resetForm(ksscore.getHtmlIdSelector('addaction'))
715
716        # set client state var 'plonetabs-addingTitle' to empty string for
717        # correct id autogeneration functionality
718        ksscore.setStateVar('plonetabs-addingTitle', '')
719
720        # remove form errors if such exist
721        self.kss_issueErrors({})
722
723        # issue portal status message
724        self.kss_issueMessage(_(u"Adding canceled."))
725
726    @kssaction
727    def kss_showEditForm(self, id, cat_name):
728        """Show edit form for given action"""
729        act_id, category, action = self.kss_validateAction(id, cat_name)
730
731        # fetch data
732        action_info = self.copyAction(action)
733        action_info["editing"] = True
734
735        # update client
736        ksscore = self.getCommandSet("core")
737        content = self.getActionsList(category=cat_name, tabs=[action_info, ])
738        ksscore.replaceHTML(ksscore.getHtmlIdSelector(id), content)
739
740        # focus name field
741        ksscore.focus(
742            ksscore.getCssSelector("#%s input[name=title_%s]" % (id, act_id)))
743
744        # issue portal status message
745        self.kss_issueMessage(_(u"Fill in required fields and click on Add."))
746
747    @kssaction
748    def kss_hideEditForm(self, id, cat_name):
749        """Hide edit form for given action"""
750        act_id, category, action = self.kss_validateAction(id, cat_name)
751
752        # update client
753        ksscore = self.getCommandSet("core")
754        content = self.getActionsList(category=cat_name, tabs=[action, ])
755        ksscore.replaceHTML(ksscore.getHtmlIdSelector(id), content)
756
757        # issue portal status message
758        self.kss_issueMessage(_(u"Changes discarded."))
759
760    @kssaction
761    def kss_editAction(self):
762        """Update action's properties"""
763        id, cat_name, data = self.parseEditForm(self.request.form)
764
765        # get category and action to edit
766        category = self.getActionCategory(cat_name)
767        action = category[id]
768
769        # validate posted data
770        errors = self.validateActionFields(cat_name, data,
771            allow_dup=(id == data['id']))
772
773        html_id = '%s%s%s' % (self.prefix, id, self.sufix)
774        ksscore = self.getCommandSet('core')
775        if not errors:
776            action = self.updateAction(id, cat_name, data)
777
778            # update client
779            # replace action item with updated one
780            content = self.getActionsList(category=cat_name, tabs=[action, ])
781            ksscore.replaceHTML(ksscore.getHtmlIdSelector(html_id), content)
782
783            message = _(u"'${id}' action successfully updated.",
784                        mapping={'id': action.id})
785            msgtype = "info"
786
787            # update page
788            self.updatePage(cat_name)
789        else:
790            # issue error messages
791            self.kss_issueErrors(errors, editform=id)
792
793            # expand advanced section if there are errors in id,
794            # action url or condition
795            if errors.has_key('id') or errors.has_key('available_expr') or \
796                errors.has_key('url_expr'):
797                self.kss_toggleCollapsible(
798                    ksscore.getCssSelector('#%s .headerAdvanced' % html_id),
799                    collapse='false')
800
801            message = _(u"Please correct the indicated errors.")
802            msgtype = "error"
803
804        # issue portal status message
805        self.kss_issueMessage(message, msgtype)
806
807    @kssaction
808    def kss_orderActions(self):
809        """Update actions order in the given category"""
810        form = self.request.form
811        cat_name = form['cat_name']
812        category = self.getActionCategory(cat_name)
813
814        # decode URI components and collect ids from request
815        components = urllib.unquote(form['actions']).split('&')
816        if self.sufix == '':
817            ids = [component[len(self.prefix):] for component in components]
818        else:
819            ids = [component[len(self.prefix):-len(self.sufix)]
820                for component in components]
821
822        # do actual sorting
823        category.moveObjectsByDelta(ids, -len(category.objectIds()))
824
825        # update client
826        self.updatePage(cat_name)
827
828        # issue portal status message
829        self.kss_issueMessage(_(u"Actions successfully sorted."))
830
831    #
832    # Utility Methods
833    #
834
835    def fixExpression(self, expr):
836        """Fix expression appropriately for tal format"""
837        if expr.find('/') == 0:
838            return 'string:${portal_url}%s' % expr
839        elif re.compile('^(ht|f)tps?\:', re.I).search(expr):
840            return 'string:%s' % expr
841        #elif re.compile('^(python:|string:|not:|exists:|nocall:|path:)',
842                        #re.I).search(expr):
843            #return expr
844        elif expr.find(':') != -1:
845            return expr
846        else:
847            return 'string:${object_url}/%s' % expr
848
849    def copyAction(self, action):
850        """Copy action to dictionary"""
851        action_info = {'description': action.description}
852        for attr in ACTION_ATTRS:
853            action_info[attr] = getattr(action, attr)
854        return action_info
855
856    def validateActionFields(self, cat_name, data, allow_dup=False):
857        """Check action fields on validity"""
858        errors = {}
859
860        if allow_dup:
861            # create dummy category to avoid id
862            # duplication during action update
863            category = ActionCategory(cat_name)
864        else:
865            # get or create (if necessary) actions category
866            category = self.getOrCreateCategory(cat_name)
867
868        # validate action id
869        chooser = INameChooser(category)
870        try:
871            chooser.checkName(data['id'], self.context)
872        except Exception, e:
873            errors['id'] = self._formatError(e, **{'id': data['id']})
874
875        # validate action name
876        if not data['title'].strip():
877            errors['title'] = _(u"Empty or invalid title specified")
878
879        # validate condition expression
880        if data['available_expr']:
881            try:
882                Expression(data['available_expr'])
883            except Exception, e:
884                mapping = {'expr': data['available_expr']}
885                idx = data['available_expr'].find(':')
886                if idx != -1:
887                    mapping['expr_type'] = data['available_expr'][:idx]
888                errors["available_expr"] = self._formatError(e, **mapping)
889
890        # validate action expression
891        if data['url_expr']:
892            try:
893                Expression(data['url_expr'])
894            except Exception, e:
895                mapping = {'expr': data['url_expr']}
896                idx = data['url_expr'].find(':')
897                if idx != -1:
898                    mapping['expr_type'] = data['url_expr'][:idx]
899                errors["url_expr"] = self._formatError(e, **mapping)
900
901        return errors
902
903    def _formatError(self, message, **kw):
904        """Make error message a little bit prettier to ease translation"""
905        charset = self._charset()
906        message = str(message)
907        message = message.replace('"', "'")
908        mapping = {}
909        for key, value in kw.items():
910            message = message.replace("'%s'" % value, "'${%s}'" % key)
911            # trying to work around zope.i18n issue
912            mapping[key] = unicode(value, charset)
913        message = message.strip()
914        return _(unicode(message, charset), mapping=mapping)
915
916    def processErrors(self, errors, prefix='', sufix=''):
917        """Add prefixes, sufixes to error ids
918        This is necessary during edit form validation,
919        because every edit form on the page has it's own sufix (id)
920        """
921        if not (prefix or sufix):
922            return errors
923
924        result = {}
925        for key, value in errors.items():
926            result['%s%s%s' % (prefix, key, sufix)] = value
927
928        return result
929
930    def parseEditForm(self, form):
931        """Extract all needed fields from edit form"""
932        # get original id and category
933        info = {}
934        id = form['orig_id']
935        category = form['category']
936
937        # preprocess 'visible' field (checkbox needs special checking)
938        if form.has_key('visible_%s' % id):
939            form['visible_%s' % id] = True
940        else:
941            form['visible_%s' % id] = False
942
943        # collect all action fields
944        for attr in ACTION_ATTRS:
945            info[attr] = form['%s_%s' % (attr, id)]
946
947        return (id, category, info)
948
949    def parseAddForm(self, form):
950        """Extract all needed fields from add form"""
951        info = {}
952        id = form['id']
953        category = form['category']
954
955        # preprocess 'visible' field (checkbox needs special checking)
956        if form.has_key('visible') and form['visible']:
957            form['visible'] = True
958        else:
959            form['visible'] = False
960
961        # fix expression fields
962        form['url_expr'] = self.fixExpression(form['url_expr'])
963
964        # collect all action fields
965        for attr in ACTION_ATTRS:
966            info[attr] = form[attr]
967
968        return (id, category, info)
969
970    def getActionCategory(self, name):
971        portal_actions = getToolByName(self.context, 'portal_actions')
972        return portal_actions[name]
973
974    def getOrCreateCategory(self, name):
975        """Get or create (if necessary) category"""
976        portal_actions = getToolByName(self.context, 'portal_actions')
977        if name not in map(lambda x: x.id,
978                           filter(lambda x: IActionCategory.providedBy(x),
979                                  portal_actions.objectValues())):
980            portal_actions._setObject(name, ActionCategory(name))
981        return self.getActionCategory(name)
982
983    def setSiteProperties(self, **kw):
984        """Change site_properties"""
985        site_properties = getToolByName(self.context,
986            "portal_properties").site_properties
987        site_properties.manage_changeProperties(**kw)
988        return True
989
990    #
991    # Utility methods for the kss actions management
992    #
993
994    def kss_validateAction(self, id, cat_name):
995        """Check whether action with given id exists in cat_name category"""
996        try:
997            category = self.getActionCategory(cat_name)
998        except Exception:
999            raise KSSExplicitError(u"%s action category does not exist." %\
1000                                   cat_name)
1001
1002        # extract action id from given list item id on client
1003        action_id = self.sufix and id[len(self.prefix):-len(self.sufix)] or \
1004                    id[len(self.prefix):]
1005        try:
1006            action = category[action_id]
1007        except Exception:
1008            raise KSSExplicitError("No %s action in %s category." %\
1009                                              (action_id, cat_name))
1010
1011        return (action_id, category, action)
1012
1013    def kss_issueErrors(self, errors, editform=False, fields=ACTION_ATTRS):
1014        """Display error messages on the client"""
1015        ksscore = self.getCommandSet('core')
1016        ts = getToolByName(self.context, 'translation_service')
1017        for field in fields:
1018            self.kss_issueFieldError(ksscore, field,
1019                                     errors.get(field, False), editform, ts)
1020
1021    def kss_issueFieldError(self, ksscore, name, error, editform, ts):
1022        """Issue this error message for the field"""
1023        if editform:
1024            id = '%s%s%s' % (self.prefix, editform, self.sufix)
1025            field_selector = ksscore.getCssSelector('#%s .edit-field-%s' % (id,
1026                UI_ATTRS.get(name, name)))
1027            field_error_selector = ksscore.getCssSelector('#%s .edit-field-%s '
1028                '.error-container' % (id, UI_ATTRS.get(name, name)))
1029        else:
1030            field_selector = ksscore.getCssSelector('form' +\
1031                '[name=addaction_form] '
1032                '.field-%s' % UI_ATTRS.get(name, name))
1033            field_error_selector = ksscore.getCssSelector('form[name='
1034                'addaction_form] .field-%s '
1035                '.error-container' % UI_ATTRS.get(name, name))
1036
1037        if error:
1038            error = ts.translate(error, context=self.context)
1039            ksscore.replaceInnerHTML(field_error_selector, error)
1040            ksscore.addClass(field_selector, 'error')
1041        else:
1042            ksscore.clearChildNodes(field_error_selector)
1043            ksscore.removeClass(field_selector, 'error')
1044
1045    def kss_toggleCollapsible(self, selector, collapsed=None,
1046                              expanded=None, collapse=None):
1047        """KSS Server command to add plonetabs-toggleCollapsible client
1048        action to response
1049        """
1050        command = self.commands.addCommand('plonetabs-toggleCollapsible',
1051                                           selector)
1052        if collapsed is not None:
1053            command.addParam('collapsed', collapsed)
1054        if expanded is not None:
1055            command.addParam('expanded', expanded)
1056        if collapse is not None:
1057            command.addParam('collapse', collapse)
1058
1059    def kss_resetForm(self, selector):
1060        """KSS Server command to reset form on client"""
1061
1062    def kss_blur(self, selector):
1063        """KSS Server command to remove focus from input"""
1064        self.commands.addCommand('plonetabs-blur', selector)
1065
1066    def kss_replaceOrInsert(self, selector, parentSelector, html,
1067                            withKssSetup='True', alternativeHTML='',
1068                            selectorType='', position='', positionSelector='',
1069                            positionSelectorType=''):
1070        """KSS Server command to execute replaceOrInsert client action"""
1071        command = self.commands.addCommand('plonetabs-replaceOrInsert',
1072            selector)
1073        command.addParam('selector', parentSelector)
1074        command.addHtmlParam('html', html)
1075        command.addParam('withKssSetup', withKssSetup)
1076        if alternativeHTML:
1077            command.addHtmlParam('alternativeHTML', alternativeHTML)
1078        if selectorType:
1079            command.addParam('selectorType', selectorType)
1080        if position:
1081            command.addParam('position', position)
1082        if positionSelector:
1083            command.addParam('positionSelector', positionSelector)
1084        if positionSelectorType:
1085            command.addParam('positionSelectorType',
1086                              positionSelectorType)
1087
1088    def kss_issueMessage(self, message, msgtype="info"):
1089        """"Issues portal status message and removes it afte 10 seconds"""
1090        ksscore = self.getCommandSet('core')
1091        self.getCommandSet('plone').issuePortalMessage(message,
1092                                                       msgtype=msgtype)
1093        self.kss_timeout(
1094            ksscore.getHtmlIdSelector('kssPortalMessage'),
1095            delay='10000', repeat='false',
1096            cmd_name='setStyle', name='display', value='none'
1097        )
1098
1099    def kss_timeout(self, selector, **kw):
1100        """KSS Server command to execute plonetabs-timeout client action"""
1101        self.commands.addCommand('plonetabs-timeout', selector, **kw)
1102
1103    def renderViewlet(self, manager, name):
1104        if isinstance(manager, basestring):
1105            manager = getMultiAdapter((self.context, self.request, self,),
1106                                      IViewletManager, name=manager)
1107        renderer = getMultiAdapter((self.context, self.request, self, manager),
1108                                   IViewlet, name=name)
1109        renderer = renderer.__of__(self.context)
1110        renderer.update()
1111        return renderer.render()
1112
1113    #
1114    # Basic API to work with portal actions tool in a more pleasent way
1115    #
1116
1117    def addAction(self, cat_name, data):
1118        """Create and add new action to category with given name"""
1119        id = data.pop('id')
1120        category = self.getOrCreateCategory(cat_name)
1121        action = Action(id, **data)
1122        category._setObject(id, action)
1123        return action
1124
1125    def updateAction(self, id, cat_name, data):
1126        """Update action with given id and category"""
1127        new_id = data.pop('id')
1128        category = self.getActionCategory(cat_name)
1129
1130        # rename action if necessary
1131        if id != new_id:
1132            category.manage_renameObject(id, new_id)
1133
1134        # get action
1135        action = category[new_id]
1136
1137        # update action properties
1138        for attr in data.keys():
1139            if data.has_key(attr):
1140                action._setPropValue(attr, data[attr])
1141
1142        return action
1143
1144    def deleteAction(self, id, cat_name):
1145        """Delete action with given id from given category"""
1146        category = self.getActionCategory(cat_name)
1147        category.manage_delObjects(ids=[id, ])
1148        return True
1149
1150    def moveAction(self, id, cat_name, steps=0):
1151        """Move action by a given steps"""
1152        if steps != 0:
1153            category = self.getActionCategory(cat_name)
1154            if steps > 0:
1155                category.moveObjectsUp([id, ], steps)
1156            else:
1157                category.moveObjectsDown([id, ], abs(steps))
1158            return True
1159        return False
1160
1161    #
1162    # KSS Methods that are used to update different parts of the page
1163    # accordingly to category
1164    #
1165
1166    def updatePage(self, category):
1167        """Seek for according method in class and calls it if found
1168        Example of making up method's name:
1169            portal_tabs => updatePortalTabsPageSection
1170        """
1171        method_name = 'update%sPageSection' % ''.join(
1172            map(lambda x: x.capitalize(), category.split('_')))
1173        if hasattr(self, method_name):
1174            getattr(self, method_name)()
1175
1176    def updatePortalTabsPageSection(self):
1177        """Method for updating global-sections on client"""
1178        ksscore = self.getCommandSet("core")
1179        self.kss_replaceOrInsert(ksscore.getHtmlIdSelector("portal-header"),
1180                                 "portal-globalnav",
1181                                 self.sections_template(),
1182                                 withKssSetup='False',
1183                                 selectorType='htmlid')
1184
1185    def updateSiteActionsPageSection(self):
1186        """Method for updating site action on client"""
1187        ksscore = self.getCommandSet("core")
1188        self.kss_replaceOrInsert(ksscore.getHtmlIdSelector("portal-header"),
1189                                 "portal-siteactions",
1190                                 self.renderViewlet("plone.portalheader",
1191                                                    "plone.site_actions"),
1192                                 withKssSetup='False',
1193                                 selectorType='htmlid',
1194                                 position="before",
1195                                 positionSelector="portal-searchbox",
1196                                 positionSelectorType="htmlid")
1197
1198        #ksszope = self.getCommandSet("zope")
1199        #ksszope.refreshViewlet(
1200           #self.getCommandSet("core").getHtmlIdSelector("portal-siteactions"),
1201           #"plone.portalheader",
1202           #"plone.site_actions")
1203
1204    def updateUserPageSection(self):
1205        """Method for updating site action on client"""
1206        ksszope = self.getCommandSet("zope")
1207        ksszope.refreshViewlet(
1208            self.getCommandSet("core").getHtmlIdSelector(
1209                "portal-personaltools-wrapper"),
1210            "plone.portaltop",
1211            "plone.personal_bar")
1212
1213
1214class PloneTabsMode(BrowserView):
1215
1216    def __call__(self):
1217        mode = self.request.get(cookie_name, False)
1218        if mode in ('plain', 'rich'):
1219            return mode
1220        mode = self.request.cookies.get(cookie_name, False)
1221        if mode in ('plain', 'rich'):
1222            return mode
1223        return 'rich'
Note: See TracBrowser for help on using the repository browser.