source: products/quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/plonetabs.py @ 917

Last change on this file since 917 was 874, checked in by mylan, 17 years ago

1.2.0 version tagged

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