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

Last change on this file since 3672 was 3672, checked in by kroman0, 11 years ago

Fixed 'PloneTabsControlPanel?.call' is too complex (11)

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