source: products/quintagroup.plonetabs/branches/plone4/quintagroup/plonetabs/browser/plonetabs.py @ 3211

Last change on this file since 3211 was 3211, checked in by gotcha, 13 years ago

take care of the case when there are no tabs

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