source: products/quintagroup.plonetabs/branches/tests/quintagroup/plonetabs/browser/plonetabs.py @ 3403

Last change on this file since 3403 was 3403, checked in by potar, 8 years ago

Fixed code according with pylint, pyflakes

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