source: products/qPloneTabs/branches/quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/plonetabs.py @ 141

Last change on this file since 141 was 141, checked in by chervol, 18 years ago

comments view updated

  • Property svn:eol-style set to native
File size: 26.6 KB
Line 
1import copy, sys
2from Acquisition import aq_inner
3from OFS.CopySupport import CopyError
4
5from zope.interface import implements
6from zope.component import getUtility, getMultiAdapter
7from zope.i18n import translate
8from zope.schema.interfaces import IVocabularyFactory
9from zope.exceptions import UserError
10from zope.app.container.interfaces import INameChooser
11
12from Products.CMFCore.utils import getToolByName
13from Products.CMFCore.interfaces import IAction, IActionCategory
14from Products.CMFCore.ActionInformation import Action, ActionCategory
15from Products.CMFCore.Expression import Expression
16from Products.CMFPlone import PloneMessageFactory as _
17from Products.CMFPlone import utils
18from Products.CMFPlone.browser.navigation import get_view_url
19from Products.Five.browser import BrowserView
20from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
21from Products.statusmessages.interfaces import IStatusMessage
22
23from plone.app.layout.navigation.root import getNavigationRoot
24from plone.app.kss.plonekssview import PloneKSSView
25from plone.app.workflow.remap import remap_workflow
26from plone.memoize.instance import memoize
27from kss.core import kssaction, KSSExplicitError
28
29from quintagroup.plonetabs.config import *
30from interfaces import IPloneTabsControlPanel
31
32ACTION_ATTRS = ["id", "title", "url_expr", "available_expr", "visible"]
33
34class PloneTabsControlPanel(PloneKSSView):
35   
36    implements(IPloneTabsControlPanel)
37   
38    template = ViewPageTemplateFile("templates/plonetabs.pt")
39    actionslist_template = ViewPageTemplateFile("templates/actionslist.pt")
40    autogenerated_template = ViewPageTemplateFile("templates/autogenerated.pt")
41   
42    # custom templates used to update page sections
43    sections_template = ViewPageTemplateFile("templates/sections.pt")
44   
45    def __call__(self):
46        """ Perform the update and redirect if necessary, or render the page """
47        postback = True
48        errors = {}
49        context = aq_inner(self.context)
50       
51        form = self.request.form
52        action = form.get("action", "")
53        submitted = form.get('form.submitted', False)
54       
55        # action handler def handler(self, form)
56        if submitted:
57            if form.has_key('add.add'):
58                postback = self.manage_addAction(form, errors)
59            elif form.has_key("edit.save"):
60                postback = self.manage_editAction(form, errors)
61            elif form.has_key("edit.delete"):
62                postback = self.manage_deleteAction(form, errors)
63            elif form.has_key("edit.moveup"):
64                postback = self.manage_moveUpAction(form, errors)
65            elif form.has_key("edit.movedown"):
66                postback = self.manage_moveDownAction(form, errors)
67            elif form.has_key("autogenerated.save"):
68                postback = self.manage_setAutogeneration(form, errors)
69            else:
70                postback = True
71       
72        if postback:
73            return self.template(errors=errors)
74   
75    ########################################
76    # Methods for processing configlet posts
77    ########################################
78   
79    def manage_setAutogeneration(self, form, errors):
80        """ Process managing autogeneration settings """
81       
82        # set excludeFromNav property for root objects
83        portal = getMultiAdapter((aq_inner(self.context), self.request), name='plone_portal_state').portal()
84        generated_tabs = form.get("generated_tabs", '0')
85        nonfolderish_tabs = form.get("nonfolderish_tabs", '0')
86       
87        for item in self.getRootTabs():
88            obj = getattr(portal, item['id'], None)
89            if obj is not None:
90                checked = form.get(item['id'], None)
91                if checked == '1':
92                    obj.update(excludeFromNav=False)
93                else:
94                    obj.update(excludeFromNav=True)
95
96        # set disable_folder_sections property
97        changeProperties = getToolByName(self.context, "portal_properties").site_properties.manage_changeProperties
98       
99        if int(generated_tabs) == 1:
100            changeProperties(disable_folder_sections=False)
101        else:
102            changeProperties(disable_folder_sections=True)
103       
104        # set disable_nonfolderish_sections property
105        if int(nonfolderish_tabs) == 1:
106            changeProperties(disable_nonfolderish_sections=False)
107        else:
108            changeProperties(disable_nonfolderish_sections=True)
109       
110        IStatusMessage(self.request).addStatusMessage(_(u"Changes saved!"), type="info")
111       
112        self.redirect()
113       
114        return False
115   
116    def manage_addAction(self, form, errs):
117        """ Manage method to add a new action to given category,
118            if category doesn't exist, create it """
119        # extract posted data
120        id, cat_name, data = self.parseAddForm(form)
121       
122        # validate posted data
123        errors = self.validateActionFields(cat_name, data)
124       
125        # if not errors find (or create) category and set action to it
126        if not errors:
127            action = self.addAction(cat_name, form)
128            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action successfully added." % action.id), type="info")
129            self.redirect(search="category=%s" % cat_name)
130            return False
131        else:
132            errs.update(errors)
133            IStatusMessage(self.request).addStatusMessage(_(u"Please correct the indicated errors."), type="error")
134            return True
135   
136    def manage_editAction(self, form, errs):
137        """ Manage Method to update action """
138        # extract posted data
139        id, cat_name, data = self.parseEditForm(form)
140       
141        # get category and action to edit
142        category = self.getActionCategory(cat_name)
143        action = category[id]
144       
145        # validate posted data
146        errors = self.validateActionFields(cat_name, data, allow_dup=True)
147       
148        if not errors:
149            action = self.updateAction(id, cat_name, data)
150            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action saved." % action.id), type="info")
151            self.redirect(search="category=%s" % cat_name)
152            return False
153        else:
154            errs.update(self.processErrors(errors, sufix='_%s' % id)) # add edit form sufix to error ids
155            IStatusMessage(self.request).addStatusMessage(_(u"Please correct the indicated errors."), type="error")
156            return True
157   
158    def manage_deleteAction(self, form, errs):
159        """ Manage Method to delete action """
160        # extract posted data
161        id, cat_name, data = self.parseEditForm(form)
162       
163        # get category and action to delete
164        category = self.getActionCategory(cat_name)
165        if id in category.objectIds():
166            self.deleteAction(id, cat_name)
167            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action deleted." % id), type="info")
168            self.redirect(search="category=%s" % cat_name)
169            return False
170        else:
171            IStatusMessage(self.request).addStatusMessage(_(u"No '%s' action in '%s' category." % (id, cat_name)), type="error")
172            return True
173   
174    def manage_moveUpAction(self, form, errs):
175        """ Manage Method for moving up given action by one position """
176        # extract posted data
177        id, cat_name, data = self.parseEditForm(form)
178       
179        # get category and action to move
180        category = self.getActionCategory(cat_name)
181        if id in category.objectIds():
182            self.moveAction(id, cat_name, steps=1)
183            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action moved up." % id), type="info")
184            self.redirect(search="category=%s" % cat_name)
185            return False
186        else:
187            IStatusMessage(self.request).addStatusMessage(_(u"No '%s' action in '%s' category." % (id, cat_name)), type="error")
188            return True
189   
190    def manage_moveDownAction(self, form, errs):
191        """ Manage Method for moving down given action by one position """
192        # extract posted data
193        id, cat_name, data = self.parseEditForm(form)
194       
195        # get category and action to move
196        category = self.getActionCategory(cat_name)
197        if id in category.objectIds():
198            self.moveAction(id, cat_name, steps=-1)
199            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action moved down." % id), type="info")
200            self.redirect(search="category=%s" % cat_name)
201            return False
202        else:
203            IStatusMessage(self.request).addStatusMessage(_(u"No '%s' action in '%s' category." % (id, cat_name)), type="error")
204            return True
205   
206    def redirect(self, url="", search="", hash=""):
207        """ Redirect to @@plonetabs-controlpanel configlet """
208       
209        if url == "":
210            portal_url =  getMultiAdapter((self.context, self.request), name=u"plone_portal_state").portal_url()
211            url = "%s/%s" % (portal_url, "@@plonetabs-controlpanel")
212       
213        if search != "":
214            search = "?%s" % search
215       
216        if hash != "":
217            hash = "#%s" % hash
218       
219        self.request.response.redirect("%s%s%s" % (url, search, hash))
220   
221    ###################################
222    #
223    #  Methods - providers for templates
224    #
225    ###################################
226   
227    def getPageTitle(self, category="portal_tabs"):
228        """ See interface """
229        portal_props = getToolByName(self.context, "portal_properties")
230        default_title = "Plone '%s' Configuration" % category
231       
232        if not hasattr(portal_props, PROPERTY_SHEET):
233            return default_title
234       
235        sheet = getattr(portal_props, PROPERTY_SHEET)
236        if not hasattr(sheet, FIELD_NAME):
237            return default_title
238       
239        field = sheet.getProperty(FIELD_NAME)
240        dict = {}
241        for line in field:
242            cat, title = line.split("|", 2)
243            dict[cat] = title
244       
245        return dict.get(category, None) or default_title
246   
247    def hasActions(self, category="portal_tabs"):
248        """ See interface """
249        return len(getToolByName(self.context, "portal_actions").listActions(categories=[category,])) > 0
250   
251    def getPortalActions(self, category="portal_tabs"):
252        """ See interface """
253        portal_actions = getToolByName(self.context, "portal_actions")
254       
255        if category not in portal_actions.objectIds():
256            return []
257       
258        actions = []
259        for item in portal_actions[category].objectValues():
260            if IAction.providedBy(item):
261                actions.append(item)
262       
263        return actions
264   
265    def isGeneratedTabs(self):
266        """ See interface """
267        site_properties = getToolByName(self.context, "portal_properties").site_properties
268        return not site_properties.getProperty("disable_folder_sections", False)
269   
270    def isNotFoldersGenerated(self):
271        """ See interface """
272        site_properties = getToolByName(self.context, "portal_properties").site_properties
273        return not site_properties.getProperty("disable_nonfolderish_sections", False)
274   
275    def getActionsList(self, category="portal_tabs", errors={}):
276        """ See interface """
277        return self.actionslist_template(category=category, errors=errors)
278   
279    def getGeneratedTabs(self):
280        """ See interface """
281        return self.autogenerated_template()
282   
283    def getRootTabs(self):
284        """ See interface """
285        context = aq_inner(self.context)
286       
287        portal_catalog = getToolByName(context, 'portal_catalog')
288        portal_properties = getToolByName(context, 'portal_properties')
289        navtree_properties = getattr(portal_properties, 'navtree_properties')
290       
291        # Build result dict
292        result = []
293       
294        # check whether we only want actions
295        if not self.isGeneratedTabs():
296            return result
297       
298        query = {}
299       
300        rootPath = getNavigationRoot(context)
301        query['path'] = {'query' : rootPath, 'depth' : 1}
302        query['portal_type'] = utils.typesToList(context)
303       
304        sortAttribute = navtree_properties.getProperty('sortAttribute', None)
305        if sortAttribute is not None:
306            query['sort_on'] = sortAttribute
307           
308            sortOrder = navtree_properties.getProperty('sortOrder', None)
309            if sortOrder is not None:
310                query['sort_order'] = sortOrder
311       
312        if navtree_properties.getProperty('enable_wf_state_filtering', False):
313            query['review_state'] = navtree_properties.getProperty('wf_states_to_show', [])
314       
315        query['is_default_page'] = False
316
317        if not self.isNotFoldersGenerated():
318            query['is_folderish'] = True
319
320        # Get ids not to list and make a dict to make the search fast
321        idsNotToList = navtree_properties.getProperty('idsNotToList', ())
322        excludedIds = {}
323        for id in idsNotToList:
324            excludedIds[id]=1
325
326        rawresult = portal_catalog.searchResults(**query)
327
328        # now add the content to results
329        for item in rawresult:
330            if not excludedIds.has_key(item.getId):
331                id, item_url = get_view_url(item)
332                data = {'name'       : utils.pretty_title_or_id(context, item),
333                        'id'         : id,
334                        'url'        : item_url,
335                        'description': item.Description,
336                        'exclude_from_nav' : item.exclude_from_nav}
337                result.append(data)
338       
339        return result
340   
341    def getCategories(self):
342        """ See interface """
343        portal_actions = getToolByName(self.context, "portal_actions")
344        return portal_actions.objectIds()
345   
346    #
347    # Methods to make this class looks like global sections viewlet
348    #
349   
350    def test(self, condition, ifTrue, ifFalse):
351        """ See interface """
352        if condition:
353            return ifTrue
354        else:
355            return ifFalse
356   
357    # methods for rendering global-sections viewlet via kss,
358    # due to bug in macroContent when global-section list is empty,
359    # ul have condition
360    def portal_tabs(self):
361        """ See global-sections viewlet """
362        actions = context_state = getMultiAdapter((self.context, self.request), name=u"plone_context_state").actions()
363        portal_tabs_view = getMultiAdapter((self.context, self.request), name="portal_tabs_view")
364       
365        return portal_tabs_view.topLevelTabs(actions=actions)
366   
367    def selected_portal_tab(self):
368        """ See global-sections viewlet """
369        selectedTabs = self.context.restrictedTraverse('selectedTabs')
370        selected_tabs = selectedTabs('index_html', self.context, self.portal_tabs())
371       
372        return selected_tabs['portal']
373   
374    ##########################
375    #
376    # KSS Server Actions
377    #
378    ##########################
379   
380    def validateAction(self, id, category, prefix="tabslist_"):
381        """ If action with given id and category doesn't exist - raise kss exception """
382        portal_actions = getToolByName(self.context, "portal_actions")
383       
384        # remove prefix, added for making ids on configlet unique ("tabslist_")
385        act_id = id[len("tabslist_"):]
386       
387        if category not in portal_actions.objectIds():
388            raise KSSExplicitError, "Unexistent root portal actions category %s" % category
389       
390        cat_container = portal_actions[category]
391        if act_id not in map(lambda x: x.id, filter(lambda x: IAction.providedBy(x), cat_container.objectValues())):
392            raise KSSExplicitError, "%s action does not exist in %s category" % (act_id, category)
393       
394        return (cat_container, act_id)
395   
396    @kssaction
397    def toggleGeneratedTabs(self, field, checked='0'):
398        """ Toggle autogenaration setting on configlet """
399       
400        changeProperties = getToolByName(self.context, "portal_properties").site_properties.manage_changeProperties
401        if checked == '1':
402            changeProperties(**{field : False})
403        else:
404            changeProperties(**{field : True})
405       
406        ksscore = self.getCommandSet("core")
407        replace_id = "roottabs"
408        content = self.getGeneratedTabs()
409       
410        ksscore.replaceInnerHTML(ksscore.getHtmlIdSelector(replace_id), content, withKssSetup="True")
411       
412        # update global-sections viewlet
413        self.updatePortalTabs()
414   
415    @kssaction
416    def toggleActionsVisibility(self, id, checked='0', category=None):
417        """ Toggle visibility for portal actions """
418        portal_actions = getToolByName(self.context, "portal_actions")
419        cat_container, act_id = self.validateAction(id, category)
420       
421        if checked == '1':
422            checked = True
423        else:
424            checked = False
425       
426        cat_container[act_id].visible = checked
427       
428        ksscore = self.getCommandSet("core")
429        if checked:
430            ksscore.removeClass(ksscore.getHtmlIdSelector(id), value="invisible")
431        else:
432            ksscore.addClass(ksscore.getHtmlIdSelector(id), value="invisible")
433       
434        self.updatePage(category)
435   
436    @kssaction
437    def toggleRootsVisibility(self, id, checked='0'):
438        """ Toggle visibility for portal root objects (exclude_from_nav) """
439        portal = getMultiAdapter((aq_inner(self.context), self.request), name='plone_portal_state').portal()
440       
441        # remove prefix, added for making ids on configlet unique ("roottabs_")
442        obj_id = id[len("roottabs_"):]
443       
444        if obj_id not in portal.objectIds():
445            raise KSSExplicitError, "Object with %s id doesn't exist in portal root" % obj_id
446       
447        if checked == '1':
448            checked = True
449        else:
450            checked = False
451       
452        portal[obj_id].update(excludeFromNav=not checked)
453       
454        ksscore = self.getCommandSet("core")
455        if checked:
456            ksscore.removeClass(ksscore.getHtmlIdSelector(id), value="invisible")
457        else:
458            ksscore.addClass(ksscore.getHtmlIdSelector(id), value="invisible")
459       
460        # update global-sections viewlet
461        self.updatePortalTabs()
462   
463    @kssaction
464    def kss_deleteAction(self, id, category):
465        """ Delete portal action with given id & category """
466        portal_actions = getToolByName(self.context, "portal_actions")
467        cat_container, act_id = self.validateAction(id, category)
468       
469        cat_container.manage_delObjects(ids=[act_id,])
470       
471        # update action list on client
472        ksscore = self.getCommandSet("core")
473       
474        ksscore.deleteNode(ksscore.getHtmlIdSelector(id))
475       
476        # add "noitems" class to Reorder controls to hide it
477        if not filter(lambda x: IAction.providedBy(x), cat_container.objectValues()):
478            ksscore.addClass(ksscore.getHtmlIdSelector("reorder"), value="noitems")
479       
480        # XXX TODO: fade effect during removing, for this kukit js action/command plugin needed
481       
482        self.updatePage(category)
483   
484    @kssaction
485    def oldAddAction(self, id, name, action='', category='portal_tabs', condition='', visible=False):
486        pass
487   
488    @kssaction
489    def editAction(self, id, category):
490        """ Show edit form for given action """
491        cat_container, act_id = self.validateAction(id, category)
492       
493        # collect data
494        action_info = self.copyAction(cat_container[act_id])
495        action_info["editing"] = True
496       
497        ksscore = self.getCommandSet("core")
498        content = self.actionslist_template(tabs=[action_info,])
499        replace_id = id
500       
501        ksscore.replaceHTML(ksscore.getHtmlIdSelector(replace_id), content)
502       
503        # focus name field
504        ksscore.focus(ksscore.getCssSelector("#%s input[name=name_%s]" % (id, act_id)))
505   
506    @kssaction
507    def editCancel(self, id, category):
508        """ Hide edit form for given action """
509        cat_container, act_id = self.validateAction(id, category)
510       
511        ksscore = self.getCommandSet("core")
512        content = self.actionslist_template(tabs=[cat_container[act_id],])
513        replace_id = id
514       
515        ksscore.replaceHTML(ksscore.getHtmlIdSelector(replace_id), content)
516   
517    #
518    # Utility Methods
519    #
520   
521    def copyAction(self, action):
522        """ Copyt action to dictionary """
523        action_info = {}
524        for attr in ACTION_ATTRS:
525            action_info[attr] = getattr(action, attr)
526        return action_info
527   
528    def validateActionFields(self, cat_name, data, allow_dup=False):
529        """ Check action fields on validity """
530        errors = {}
531       
532        if allow_dup:
533            category = ActionCategory(cat_name)           # create dummy category to avoid id duplication during action update
534        else:
535            category = self.getOrCreateCategory(cat_name) # get or create (if necessary) actions category
536       
537        # validate action id
538        chooser = INameChooser(category)
539        try:
540            chooser.checkName(data['id'], self.context)
541        except Exception, e:
542            errors['id'] = "%s" % str(e)
543       
544        # validate action name
545        if not data['title'].strip():
546            errors['title'] = 'Empty or invalid title specified'
547       
548        # validate condition expression
549        if data['available_expr']:
550            try:
551                Expression(data['available_expr'])
552            except Exception, e:
553                errors["available_expr"] = "%s" % str(e)
554       
555        # validate action expression
556        if data['url_expr']:
557            try:
558                Expression(data['url_expr'])
559            except Exception, e:
560                errors["url_expr"] = "%s" % str(e)
561       
562        return errors
563   
564    def processErrors(self, errors, prefix='', sufix=''):
565        """ Add prefixes, sufixes to error ids
566            This is necessary during edit form validation,
567            because every edit form on the page has it's own sufix (id) """
568        if not (prefix or sufix):
569            return errors
570       
571        result = {}
572        for key, value in errors.items():
573            result['%s%s%s' % (prefix, key, sufix)] = value
574       
575        return result
576   
577    def parseEditForm(self, form):
578        """ Extract all needed fields from edit form """
579        # get original id and category
580        info = {}
581        id = form['orig_id']
582        category = form['category']
583       
584        # preprocess 'visible' field (checkbox needs special checking)
585        if form.has_key('visible_%s' % id):
586            form['visible_%s' % id] = True
587        else:
588            form['visible_%s' % id] = False
589       
590        # collect all action fields
591        for attr in ACTION_ATTRS:
592            info[attr] = form['%s_%s' % (attr, id)]
593       
594        return (id, category, info)
595   
596    def parseAddForm(self, form):
597        """ Extract all needed fields from add form """
598        info = {}
599        id = form['id']
600        category = form['category']
601       
602        # preprocess 'visible' field (checkbox needs special checking)
603        if form.has_key('visible'):
604            form['visible'] = True
605        else:
606            form['visible'] = False
607       
608        # collect all action fields
609        for attr in ACTION_ATTRS:
610            info[attr] = form[attr]
611       
612        return (id, category, info)
613   
614    def getActionCategory(self, name):
615        portal_actions = getToolByName(self.context, 'portal_actions')
616        return portal_actions[name]
617   
618    def getOrCreateCategory(self, name):
619        """ Get or create (if necessary) category """
620        portal_actions = getToolByName(self.context, 'portal_actions')
621        if name not in map(lambda x: x.id, filter(lambda x: IActionCategory.providedBy(x), portal_actions.objectValues())):
622            portal_actions._setObject(name, ActionCategory(name))
623        return self.getActionCategory(name)
624   
625    def addAction(self, cat_name, data):
626        """ Create and add new action to category with given name """
627        id = data.pop('id')
628        category = self.getOrCreateCategory(cat_name)
629        action = Action(id, **data)
630        category._setObject(id, action)
631        return action
632   
633    def updateAction(self, id, cat_name, data):
634        """ Update action with given id and category """
635        new_id = data.pop('id')
636        category = self.getActionCategory(cat_name)
637       
638        # rename action if necessary
639        if id != new_id:
640            category.manage_renameObject(id, new_id)
641       
642        # get action
643        action = category[new_id]
644       
645        # update action properties
646        for attr in ACTION_ATTRS:
647            if data.has_key(attr):
648                action._setPropValue(attr, data[attr])
649       
650        return action
651   
652    def deleteAction(self, id, cat_name):
653        """ Delete action with given id from given category """
654        category = self.getActionCategory(cat_name)
655        category.manage_delObjects(ids=[id,])
656        return True
657   
658    def moveAction(self, id, cat_name, steps=0):
659        """ Move action by a given steps """
660        if steps != 0:
661            category = self.getActionCategory(cat_name)
662            if steps > 0:
663                category.moveObjectsUp([id,], steps)
664            else:
665                category.moveObjectsDown([id,], abs(steps))
666            return True
667        return False
668   
669    #
670    # KSS Methods that are used to update different parts of the page
671    # according to category
672    #
673   
674    def updatePage(self, category):
675        """ Seek for according method in class and calls it if found
676            Example of making up method's name:
677                portal_tabs => updatePortalTabs """
678        method_name = 'update%sPageSection' % ''.join(map(lambda x: x.capitalize(), category.split('_')))
679        if hasattr(self, method_name):
680            getattr(self, method_name)()
681   
682    def updatePortalTabsPageSection(self):
683        """ Method for updating global-sections on client """
684        ksscore = self.getCommandSet("core")
685        ksscore.replaceHTML(
686            ksscore.getHtmlIdSelector("portal-globalnav"),
687            self.sections_template(),
688            withKssSetup="False")
689   
690    def updateSiteActionsPageSection(self):
691        """ Method for updating site action on client """
692        ksszope = self.getCommandSet("zope")
693        ksszope.refreshViewlet(
694            self.getCommandSet("core").getHtmlIdSelector("portal-siteactions"),
695            "plone.portalheader",
696            "plone.site_actions")
697   
698    def updateUserPageSection(self):
699        """ Method for updating site action on client """
700        ksszope = self.getCommandSet("zope")
701        ksszope.refreshViewlet(
702            self.getCommandSet("core").getHtmlIdSelector("portal-personaltools-wrapper"),
703            "plone.portaltop",
704            "plone.personal_bar")
705
706
Note: See TracBrowser for help on using the repository browser.