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

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