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

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

Fixing pep8

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