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

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

fix adding portal_tabs; whitespace

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