source: trunk/ConfigurationMethods.py @ 1704

Revision 1704, 24.9 KB checked in by pjotr, 13 years ago (diff)

Trash folder viewable for anyone, folder view will only show stuff for managers

  • Property svn:eol-style set to native
Line 
1# Copyright 2006 by the LeMill Team (see AUTHORS)
2#
3# This file is part of LeMill.
4#
5# LeMill is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# LeMill is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with LeMill; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18
19# -*- coding: iso-8859-1 -*-
20
21from Acquisition import aq_get
22from AccessControl import Permissions, getSecurityManager
23from zExceptions import BadRequest
24from OFS.PropertyManager import PropertyManager
25from zLOG import INFO, ERROR
26from Globals import package_home
27
28from Products.CMFCore.utils import getToolByName
29from Products.CMFCore.DirectoryView import registerDirectory, addDirectoryViews
30from Products.CMFCore.ActionInformation import ActionInformation
31from Products.CMFPlone.migrations.migration_util import safeEditProperty
32from Products.CMFPlone.PloneFolder import addPloneFolder
33from Products.CMFPlone.setup.SetupBase import SetupWidget
34from Products.SiteErrorLog.SiteErrorLog import manage_addErrorLog
35from Products.Archetypes.public import listTypes, registerType
36from Products.Archetypes.Extensions.utils \
37     import installTypes, install_subskin
38from Products.PythonScripts.PythonScript import PythonScript
39
40from DateTime import DateTime
41import string, types
42from cStringIO import StringIO
43
44from config import *
45
46
47# All methods starting with "setup" will be used in the customization policy
48# The methods will be sorted by ascii value, so if something  needs to happen before
49# something else, make it so (see method setup001Dependencies).
50
51# Note that all methods need to be repeatable - they should not fail
52# if they are run again when all that they do has already been done.
53# Also, the methods should be able to upgrade, meaning that older customizations
54# should be changed to match the newest specifications.
55
56def setup001Dependencies(self, portal):
57    """Install all necessary products into the plone instance."""
58    qi=getToolByName(portal, 'portal_quickinstaller')
59    # We need ourselves installed into the plone instance,
60    # since we provide portal tools and content
61    qi.installProduct('LeMill')
62    # LanguageTool is also necessary
63    qi.installProduct('PloneLanguageTool')
64
65def setupSkin(self,portal):
66    """Create new custom skin."""
67    skinsTool = getToolByName(portal,"portal_skins")
68
69    # Register our own skin folder as a viewable folder
70    try:
71        addDirectoryViews(skinsTool,SKINS_DIR,GLOBALS)
72    except:
73        # If the directory is already registered, we just continue
74        pass
75
76    # Create our new skin, making it a copy of BASE_SKIN and adding
77    # our skin layer just after "custom".
78    for SKIN_NAME in SKIN_NAMES:
79        if SKIN_NAME not in skinsTool.getSkinSelections():
80            path = skinsTool.getSkinPath(BASE_SKIN)
81            path = map(string.strip,string.split(path,','))
82            for skinFolder in SKIN_COMMON_FOLDERS + [SKIN_NAME.lower().replace(' ','_'),]:
83                if skinFolder not in path:
84                    try:
85                        path.insert(path.index('custom')+1,skinFolder)
86                    except ValueError:
87                        path.append(skinFolder)
88            path=string.join(path,',')
89            skinsTool.addSkinSelection(SKIN_NAME,path)
90
91    # Select our first skin as default
92    skinsTool.default_skin=SKIN_NAMES[0]
93
94def addObject(portal,id,type,title,desc=None):
95    """Convenience method for creating new content items."""
96    try:
97        portal.invokeFactory(id=id,type_name=type)
98    except BadRequest:
99        # If the id already existed, we'll just quietly be happy about it.
100        pass
101    finalizeObject(portal,id,title,desc)
102
103def finalizeObject(portal,id,title,desc=None):
104    """Set content object properties in place."""
105    ob=getattr(portal,id)
106   
107    # Set basic properties of object
108    ob.setTitle(to_unicode(title))
109    if desc:
110        ob.setDescription(to_unicode(desc))
111       
112    # Publish content
113    ob.content_status_modify(workflow_action='publish')
114
115def setupFrontPage(self,portal):
116    """Setup the front page."""
117    # Make sure that old PythonScript for redirecting isn't there anymore
118    # the content section.
119    try:
120        portal._delObject('front-page')
121    except AttributeError:
122        pass
123                 
124def setupFolders(self,portal):
125    """Setup the basic structure of the site."""
126    atool = getToolByName(portal, 'portal_actions')
127    acts = atool._cloneActions()
128    # Clean up any portal_tabs from actions
129    new_acts=[]
130    for action in acts:
131        if action.category == 'portal_tabs':
132            pass
133        else:
134            new_acts.append(action)
135
136    # Loop through all main sections
137    for item in SECTIONS:
138        # Create folder
139         
140        if item in SECTION_FOLDER_TYPES:
141            if not hasattr(portal, item.lower()):
142                addObject(portal,item.lower(),SECTION_FOLDER_TYPES[item],item)
143            folder = getattr(portal,item.lower())
144            folder.manage_permission(ADD_CONTENT_PERMISSION, ('Member',), acquire=1)
145            folder.manage_permission(LIST_FOLDER_CONTENTS, ('Member',), acquire=1)
146            folder.manage_permission(ADD_TOPICS, ('Member',), acquire=1)
147           
148        # Set up properties for folder
149        if SECTION_PROPERTIES[item]:
150            for property in SECTION_PROPERTIES[item].keys():
151                value=SECTION_PROPERTIES[item][property]
152                if type(value) == type(True):
153                    dtype='boolean'
154                else:
155                    dtype='lines'
156                safeEditProperty(obj=getattr(portal,item.lower()),
157                    key=property,
158                    value=value,
159                    data_type=dtype)
160               
161        # Add action for portal tab
162        new_acts.append(ActionInformation(id=item.lower(),
163                                          title=item,
164                                          description=item,
165                                          category='portal_tabs',
166                                          permissions=('View',),
167                                          visible=True,
168                                          action='string:$portal_url/%s' % item.lower()))
169    atool._actions=new_acts
170
171    # Setup additional Trash folder for deleted resources
172    # It might be a good idea to make any actions including View only usable by users with Manager permission
173    trash_folder = 'Trash'
174    trash_type = 'LargeTrashFolder'
175
176    if not hasattr(portal, trash_folder.lower()):
177        addObject(portal,trash_folder.lower(),trash_type,trash_folder)
178    folder = getattr(portal,trash_folder.lower())
179    folder.manage_permission(ADD_CONTENT_PERMISSION, ('Member',), acquire=1)
180    folder.manage_permission(LIST_FOLDER_CONTENTS, ('Member',), acquire=1)
181    folder.manage_permission(ADD_TOPICS, ('Member',), acquire=1)
182    folder.manage_permission('View', ('Manager', 'Anonymous',), acquire=1)
183
184def setupGroups(self, portal):
185    """Set groups to use Blogs as workspaces"""
186    grouptool=getToolByName(portal, "portal_groups")
187
188    if not grouptool.getGroupWorkspacesCreationFlag():
189        grouptool.toggleGroupWorkspacesCreation()
190    grouptool.setGroupWorkspacesFolder(id=MEMBERS_FOLDER.lower(), title=MEMBERS_FOLDER)
191    grouptool.setGroupWorkspaceType('GroupBlog')
192    grouptool.manage_permission('Add Groups', ('Manager','Member'), acquire=1)
193    grouptool.manage_permission('Delete Groups', ('Manager','Owner'), acquire=1)
194    grouptool.manage_permission('Manage Groups', ('Manager','Owner','Member'), acquire=1)
195
196
197def setupMembersFolder(self,portal):
198    """Set members folder to something else (MEMBERS_FOLDER= 'community')""" 
199    membertool=getToolByName(portal,"portal_membership")
200    membertool.setMembersFolderById(id=MEMBERS_FOLDER.lower())
201    membertool.setMemberAreaType('MemberFolder')
202
203def setupToptabs(self,portal):
204    # Disable automatic tabs for top level folders
205    stp = getToolByName(portal,'portal_properties').site_properties
206    safeEditProperty(stp,'disable_folder_sections',True)
207
208    # Hide all tabs that aren't what we want
209    atool = getToolByName(portal, 'portal_actions')
210    # Hide all tabs that aren't needed
211    for a in atool._actions:
212        if a.getCategory() in ('portal_tabs',):
213            # Don't hide the tabs that we've created (specified in "SECTIONS")
214            if a.Title() not in SECTIONS:
215                a.visible=0
216
217
218def setupJavascripts(self, portal):
219    """Install new scripts"""
220    jsreg = getToolByName(portal, 'portal_javascripts', None)
221    script = 'js_helpers.js'
222    if jsreg is not None:
223        script_ids = jsreg.getResourceIds()
224        # Failsafe: first make sure the stylesheet doesn't exist in the list
225        if script not in script_ids:
226            jsreg.registerScript(script)
227            # put it at the bottom of the stack
228            jsreg.moveResourceToBottom(script)   
229#        script = 'js_helpers.js'
230#        if script not in script_ids:
231#            jsreg.registerScript(script)
232#            jsreg.moveResourceToBottom(script)   
233
234
235def setupPortlets(self,portal):
236    """Sets up the basic portlet layout."""
237
238    # Use safeEditProperty always to edit properties of the portal or any tools
239    safeEditProperty(portal,'left_slots',('here/portlet_empty/macros/portlet',))
240    safeEditProperty(portal,'right_slots',('',))
241
242def setupSiteSyndication(self, portal):
243    # Enable syndication
244    syn_tool = getToolByName(portal, 'portal_syndication')
245    syn_tool.editProperties(isAllowed=True)
246    #safeEditProperty(syn_tool,'isAllowed',True)
247
248
249def setupUsers(self,portal):
250    mdat_tool = getToolByName(portal, 'portal_memberdata')
251    mship_tool = getToolByName(portal, 'portal_membership')
252    for prop in MEMBER_PROPERTIES:
253        if not hasattr(mdat_tool, prop[0]):
254            mdat_tool.manage_addProperty(prop[0],'',prop[1])
255    # default_portrait seems to be in two places. Hope some of these get the work done.
256    portal.manage_changeProperties(default_portrait=DEFAULT_PORTRAIT)
257    portal.default_portrait=DEFAULT_PORTRAIT
258    mship_tool.manage_changeProperties(default_portrait=DEFAULT_PORTRAIT)
259    mship_tool.default_portrait=DEFAULT_PORTRAIT
260   
261def setupCatalog(self,portal):
262    # put 'language' and 'subject' in catalog index and retrieved metadata
263    # from http://plone.org/documentation/how-to/adding-new-fields-to-smart-folders-search
264    # these are metadata fields from basic plone objects.
265    class args:
266            def __init__(self, **kw):
267                self.__dict__.update(kw)
268            def keys(self):
269                return self.__dict__.keys()
270
271
272    catalog_tool = getToolByName(portal, 'portal_catalog')
273    try:
274        catalog_tool._removeIndex("Language")
275    except:
276        pass
277    try:
278        catalog_tool.delColumn("Language")
279    except:
280        pass
281
282    extra = args(doc_attr='Language',
283                 lexicon_id='plone_lexicon',
284                 index_type='Okapi BM25 Rank')
285    catalog_tool.manage_addIndex("Language", "FieldIndex", extra)
286    catalog_tool.manage_addColumn("Language")
287    try:
288        catalog_tool.delColumn("sortable_title")
289    except:
290        pass
291    catalog_tool.manage_addColumn("sortable_title")   
292    try:
293        catalog_tool.delColumn("UID")
294    except:
295        pass
296    catalog_tool.manage_addColumn("UID")
297       
298
299
300def setup002Workflows(self, portal):
301    """Setup custom workflows."""
302    wtool = getToolByName(portal, 'portal_workflow')
303    for workflow in ['lemill_workflow','group_workflow','wikish_workflow','personal_workflow']:
304        try:
305            wtool.manage_delObjects(workflow)
306        except:
307            pass
308    wtool.manage_addWorkflow('group_workflow (LeMill group workflow)', 'group_workflow')
309    wtool.manage_addWorkflow('wikish_workflow (LeMill wiki-like workflow)', 'wikish_workflow')
310    wtool.manage_addWorkflow('personal_workflow (LeMill personal workflow)', 'personal_workflow')
311    wtool.setChainForPortalTypes(MATERIAL_TYPES+('GroupBlog',), 'group_workflow') # here LeMillReferences get wrong workflow
312    wtool.setChainForPortalTypes(('Activity', 'Tool','Piece','LeMillReference'), 'wikish_workflow') # here they get fixed
313    wtool.setChainForPortalTypes(('Story', 'MemberFolder', 'BlogPost', 'Collection',), 'personal_workflow')
314    # Update all resources to have permissions that match the current workflow spec
315    wtool.updateRoleMappings()
316    portal.manage_permission('Review portal content', ('Manager','Owner','Reviewer'), acquire=1)
317
318
319def setupTopics(self, portal):
320    """Setup pre-defined searches inside SECTIONS to provide a browsable lists of contents."""
321
322    portal_types=getToolByName(portal, 'portal_types')
323    if hasattr(portal_types, 'Topic'):
324        methodlist=portal_types.Topic.getAvailableViewMethods(None)
325        if DEFAULT_TOPIC_VIEW not in methodlist:
326            methodlist=methodlist+(DEFAULT_TOPIC_VIEW,)
327        portal_types.Topic.manage_changeProperties(view_methods=methodlist)
328        portal_types.Topic.manage_changeProperties(default_view=DEFAULT_TOPIC_VIEW)
329    # Loop through all main sections
330
331    for foldername in SECTIONS:
332        if SECTION_TOPICS.has_key(foldername):
333            # additional loop for having several kinds of type restrictions
334            for topic_type in SECTION_TOPICS[foldername]:
335                generated_topics = topic_type[1]
336                allowed_types = topic_type[0]
337                # Loop through topics inside folders
338                if SECTION_TOPICS[foldername]:
339                    folder = getattr(portal, foldername.lower())
340                    for topic_conf in generated_topics:
341                        # Create topics with specific queries
342                        if hasattr(folder.aq_base, topic_conf['id']):
343                            folder._delObject(topic_conf['id'])
344                        addObject(folder, topic_conf['id'], 'Topic', topic_conf['title'])
345                        # Set up properties for topics
346                        topic = getattr(folder, topic_conf['id'])
347                        topic.manage_permission(ADD_CONTENT_PERMISSION, ('Member',), acquire=1)
348                        topic.manage_permission(LIST_FOLDER_CONTENTS, ('Member',), acquire=1)
349                        sortby='sortable_title'
350                        if topic_conf.has_key('sortby'):
351                            sortby=topic_conf['sortby']
352                        rev = False
353                        if topic_conf.has_key('reversed'):
354                            rev=topic_conf['reversed']
355                        topic.setSortCriterion(sortby, reversed=rev)
356                        getmethod='sortable_title'
357                        if topic_conf.has_key('getmethod'):
358                            getmethod=topic_conf['getmethod']
359                        topic.getmethod=getmethod
360                        criterion = topic.addCriterion('Type', 'ATPortalTypeCriterion' )
361                        criterion.setValue(allowed_types)
362                        # Set additional criterions for drafts and publisheds
363                        if topic_conf.has_key('criterions'):
364                            for crit,crittype,value in topic_conf['criterions']:
365                                criterion = topic.addCriterion(crittype, crit)
366                                if not value=='':
367                                    criterion.setValue(value)
368                        else:
369                            criterion = topic.addCriterion('review_state','ATSelectionCriterion')
370                            criterion.setValue(('public','draft'))   
371                        if topic_conf['id'] == 'recent':
372                            crit = topic.addCriterion('getLatestEdit', 'ATFriendlyDateCriteria')
373                            crit.setOperation('less')
374                            crit.setDateRange('-')
375                            crit.setValue('3')
376                        if topic_conf['id'] == 'portfolio':
377                            if not hasattr(topic.aq_base, 'left_slots'):
378                                topic._setProperty('left_slots', ['here/portlet_portfolio/macros/backlinks',], 'lines')
379
380
381def setupRemoteLeMilles(self, portal):
382    #If variable set, tries to add REMOTE_SERVERS to be included in LeMill searches
383    search_tool=getToolByName(portal, 'lemill_search')
384    remote_boxes=search_tool.get_remote_lemilles()
385    for box in REMOTE_SERVERS:
386        add = 1
387        for r in remote_boxes.values():
388            if box == r['URL']:
389                add = 0
390        if add:
391            search_tool.setNewLocation(box)
392
393
394def setupCleanDuplicateActions(self, portal):
395    # Migrating Plone or LeMill causes duplication of certain actions. This is Plone's fault.
396    # addNewActions in Plone's ConfigurationMethods creates these actions without checking if they already exist.
397    atool=getToolByName(portal, 'portal_actions')
398    acts = atool._cloneActions()
399    clean_acts=[]
400    home_check = ownership_check = rename_check = paste_check = delete_check = 0
401    # Go through actions and add only first instances of the following actions to the new actions list:
402    for a in acts:
403        if a.getId()=='index_html': #'Home'
404            if home_check==0:
405                home_check=1
406                clean_acts.append(a)
407        elif a.getId()=='change_ownership':
408            if ownership_check==0:
409                ownership_check=1
410                clean_acts.append(a)
411        elif a.getId()=='rename':
412            if rename_check==0:
413                rename_check=1
414                clean_acts.append(a)
415        elif a.getId()=='paste':
416            if paste_check==0:
417                paste_check=1
418                clean_acts.append(a)
419        elif a.getId()=='delete':
420            if delete_check==0:
421                delete_check=1
422                clean_acts.append(a)
423        else:
424            clean_acts.append(a)
425    atool._actions = tuple( clean_acts )
426
427def setupFactoryTypes(self, portal):
428    """ set types that should use portal_factory """
429    ft = getToolByName(portal, 'portal_factory')
430    types_list = ft.getFactoryTypes()
431    for t in ALL_CONTENT_TYPES:
432        types_list[t] = 1
433    ft.manage_setPortalFactoryTypes(None, types_list)
434
435def setupConfigureKupu(self, portal):
436    """ configure kupu """
437    k = getToolByName(portal, 'kupu_library_tool')
438    k.configure_kupu(1, '',  #linkbyuid, table_classnames
439            [   # html exclusion. list tags you don't want to be in HTML code
440                {'tags':TAGS_BLACKLIST,
441                 'attributes':'',
442                 'keep':1,
443                }, # list attributes you don't want in HTML code
444                {'tags':'',
445                 'attributes':'dir,lang,valign,halign,border,frame,rules,cellspacing,cellpadding,bgcolor',
446                 'keep':1,
447                },
448            ],
449            ['text-align', 'list-style-type'],   # style whitelist
450            [],  # class_blacklist
451            1,  # install before unload
452            ['Heading|h2', 'Code|pre'])   # Kupu can't handle 'code'
453
454language_list = ['cz','en','es','et','fi','hu','lt','ru','se']
455# Still missing: ['de','nl','pl','sl']
456
457def setupLanguageTool(self, portal):
458    """ configure plone language tool to use browser language request negotation """
459    lt = getToolByName(portal, 'portal_languages')
460    if lt is None:
461        return
462    lt.addLanguage('cz', 'Czech')
463    lt.manage_setLanguageSettings('en', #default language
464            language_list, # supported languages (needs to be a list, not a tuple)
465            1, # use cookie negotiation
466            1, # use request negotiation
467            0, # use path negotiation
468            0, # force language URLs (content)
469            1, # allow fallback (content)
470            1, # use combined languages
471            0, # display flags
472            0, # start neutral (content)
473            )
474
475# TODO: when we get too many translations, we'll need to shorten the primary list
476primary_language_list = language_list
477
478def setupProperties(self, portal):
479    lt = getToolByName(portal, 'lemill_tool')
480    safeEditProperty(lt, 'portal_integration', value=False, data_type="boolean")
481    safeEditProperty(lt, 'portal_search_link', value="TODO", data_type="string")
482    safeEditProperty(lt, 'primary_languages', value=primary_language_list, data_type="tokens")
483    safeEditProperty(lt, 'allow_banning', value=False, data_type="boolean")
484    safeEditProperty(lt, 'irc_link', value='http://lemill.net/irc/irc.cgi', data_type="string")
485    lt.language_dict=dict(portal.availableLanguages())
486    safeEditProperty(portal, 'validate_email', value=True, data_type="boolean")
487   
488def setupRoles(self, portal):
489    portal._addRole('CoAuthor')
490
491
492def setupLeMillFAQ(self, portal):
493    # Create MultimediaMaterial resource lemill-faq based on docs/FAQ.html
494    faqid='lemill-faq'
495    if not hasattr(portal.content, faqid):
496        addObject(portal.content, faqid, 'MultimediaMaterial', 'LeMill FAQ')
497    else:
498        return 1 # If there is a lemill_faq don't overwrite it, because we don't want to lose community's changes
499    faq=getattr(portal.content, faqid)   
500    homedir=package_home(globals())
501    file=open(homedir+'/docs/FAQ.html', 'r').read()
502    title=file[file.find('<title>')+7:file.rfind('</title>')]
503    body=file[file.find('<body>')+6:file.rfind('</body>')]
504    faq.edit(title=title, bodyText=body, language='en', tags=['faq', 'lemill'])
505    faq.manage_afterAdd(faq,portal.content)
506    faq.at_post_create_script()
507    faq.reindexObject()
508       
509       
510def toolboxSetupCustomization(self, portal):
511    # Do necessary additional customization steps
512
513    # Use EUN skin
514    skinsTool = getToolByName(portal,"portal_skins")
515    skinsTool.default_skin="LeMill EUN"
516   
517    # Setup basic properties
518    lt = getToolByName(portal, 'lemill_tool')
519    safeEditProperty(lt, 'portal_integration', value=True)
520    safeEditProperty(lt, 'portal_search_link', value="http://calibrate.eun.org/merlin/index.cfm?fuseaction=resources.dsp_search")
521    # Official CALIBRATE languages, whether implemented or not
522    language_list = ['cz','de','en','et','hu','lt','nl','pl','sl']
523    safeEditProperty(lt, 'primary_languages', value=language_list, data_type="tokens")
524
525    lt = getToolByName(portal, 'portal_languages')
526    lt.addLanguage('cz', 'Czech')
527    lt.manage_setLanguageSettings('en', #default language
528            language_list,
529            1, # use cookie negotiation
530            1, # use request negotiation
531            0, # use path negotiation
532            0, # force language URLs (content)
533            1, # allow fallback (content)
534            1, # use combined languages
535            0, # display flags
536            0, # start neutral (content)
537            )
538    # Disable Join functionality
539    portal.manage_permission('Add portal member',('Manager',))
540
541    # Replace front-page with a PythonScript that does a redirection to
542    # the content section.
543    try:
544        portal._delObject('front-page')
545    except AttributeError:
546        pass
547    id = portal._setObject('front-page',PythonScript('front-page'))
548    getattr(portal,id).write(
549    """request = container.REQUEST
550RESPONSE =  request.RESPONSE
551RESPONSE.redirect(container.absolute_url()+'/content/') """)
552
553#########
554# Collect all setup functions into a setup widget
555
556afunctions = {}
557for f in dir():
558    if f.startswith('setup'):
559        func = eval("%s" % f)
560        if type(func) == types.FunctionType:
561            afunctions[f] = func
562
563toolbox_functions = {}
564for f in dir():
565    if f.startswith('toolboxSetup'):
566        func = eval("%s" % f)
567        if type(func) == types.FunctionType:
568            toolbox_functions[f] = func
569
570
571class LeMillSetup(SetupWidget):
572    type = 'LeMill Setup'
573
574    description = """Customization methods needed by the LeMill Plone portal"""
575
576    functions = afunctions
577
578    def setup(self):
579        pass
580
581    def delItems(self, fns):
582        out = []
583        out.append(('Currently there is no way to remove a function', INFO))
584        return out
585
586    def addItems(self, fns):
587        """This method is called when configuration methods need to be applied.
588        fns is the list of function names that need to be executed in order."""
589        out = []
590        for fn in fns:
591            # All functions are executed with the portal as the first actual parameter
592            self.functions[fn](self, self.portal)
593            out.append(('Function %s has been applied' % fn, INFO))
594        return out
595
596    def installed(self):
597        return []
598
599    def available(self):
600        """Get a list of availabel functions."""
601        funcs = self.functions.keys()
602        # Sort, so we get a pre-determined order.
603        # The functions need to be named properly
604        funcs.sort()
605        return funcs
606
607
608class ToolboxSetup(LeMillSetup):
609    type = 'Toolbox Setup'
610
611    description = """Customization methods needed by the Toolbox"""
612
613    functions = {}
614    functions.update(afunctions)
615    functions.update(toolbox_functions)
616
Note: See TracBrowser for help on using the repository browser.