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

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

Fixed compatibility with plone3

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