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

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

Added detecting plone version

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