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

Last change on this file since 3608 was 3608, checked in by vmaksymiv, 11 years ago

PPP fixes

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