source: indico/indico/MaKaC/plugins/base.py @ 9264b3

hello-world-walkthroughipv6v0.98-seriesv0.98.2v0.98.3v0.98b2v0.99v1.0v1.1
Last change on this file since 9264b3 was 9264b3, checked in by Jose Benito <jose.benito.gonzalez@…>, 2 years ago

[REF] indico.core.api to indico.core.extpoint

  • Property mode set to 100644
File size: 51.7 KB
Line 
1# -*- coding: utf-8 -*-
2##
3##
4## This file is part of CDS Indico.
5## Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 CERN.
6##
7## CDS Indico is free software; you can redistribute it and/or
8## modify it under the terms of the GNU General Public License as
9## published by the Free Software Foundation; either version 2 of the
10## License, or (at your option) any later version.
11##
12## CDS Indico is distributed in the hope that it will be useful, but
13## WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15## General Public License for more details.
16##
17## You should have received a copy of the GNU General Public License
18## along with CDS Indico; if not, write to the Free Software Foundation, Inc.,
19## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20
21""" This purpose of this file is high-level handling of plugins.
22    It has functions to store and retrieve information from the plugins that is stored in the database;
23    for example, which plugins are active or not, options declared by plugins and their values, etc.
24    The functions can be used through methods of the "PluginsHolder" class. See its description for more details.
25"""
26
27from MaKaC.common.Counter import Counter
28from BTrees.OOBTree import OOBTree
29from MaKaC.common.Locators import Locator
30from MaKaC.errors import PluginError
31from MaKaC.common import DBMgr
32from MaKaC.common.logger import Logger
33import zope.interface, types
34from persistent import Persistent
35import pkg_resources, types, inspect, re
36
37from MaKaC.plugins.loader import PluginLoader, GlobalPluginOptions
38from MaKaC.common.ObjectHolders import ObjectHolder
39from MaKaC.plugins.util import processPluginMetadata
40
41from indico.core.extpoint import Component, IListener, IContributor
42from indico.web import rh as newrh
43
44
45def pluginId(mod):
46    """
47    Takes a module and spits out its plugin id
48    """
49    return mod.__name__.split('.')[-1]
50
51
52class Observable(object):
53    def _notify(self, event, *params):
54        return PluginsHolder().getComponentsManager().notifyComponent(event, self, *params)
55
56
57class OldObservable:
58    """
59    Version for old style classes
60    """
61    def _notify(self, event, *params):
62        return PluginsHolder().getComponentsManager().notifyComponent(event, self, *params)
63
64
65class ComponentsManager(Persistent):
66    '''
67    Manages all what is related to the components.
68
69    eventComponentsDict keeps a record of the components that implement a certain event, and is used to register and unregister
70    events when a plugin or a pluginType is either activated or desactivated.
71    components is a list with all the components found in MaKaC/plugins, it is updated every time that the method loadPlugins is called.
72    '''
73
74    def __init__(self):
75        self.__id = "componentsManager"
76        self.__eventComponentsDict = {}
77        self.__components = []
78
79    def getId(self):
80        return self.__id
81
82    def addComponent(self, component):
83        if component is not None and component not in self.__components:
84            self.__components.append(component)
85        self._notifyModification()
86
87    def registerAllComponents(self, pluginType = None):
88        '''We register all the components, but if a plugintype is passed we should just register the changes
89        in the part referred to that plugintype'''
90
91        for component in self.__components:
92            pluginTypeName, possiblePluginName = self.getAssociatedPlugin(component)
93
94            #it's a plugin
95            if possiblePluginName:
96                isActive = PluginsHolder().getPluginType(pluginTypeName).getPlugin(possiblePluginName).isActive()
97                #if the pluginType that the plugin belongs to is not active we won't register the components for the plugin
98                if not PluginsHolder().getPluginType(pluginTypeName).isActive():
99                    isActive = False
100
101            #it's a pluginType
102            else:
103                isActive = PluginsHolder().getPluginType(pluginTypeName).isActive()
104
105            if isActive:
106                self.registerComponent(component)
107
108    def registerComponent(self, component):
109        #Register it. We take the list of the interfaces implemented by the component
110        implementedInterfaces = list(zope.interface.implementedBy(component))
111
112        for interface in implementedInterfaces:
113
114            #we take the list of the methods in the interface
115            for met in list(interface):
116                #if the method is implemented in the component, then we register it
117                if met in dir(component):
118                    self.registerNewEvent(met , component)
119
120    def registerNewEvent(self, event, componentClass):
121        changed = False
122        if event not in self.__eventComponentsDict:
123            self.__eventComponentsDict[event] = []
124            changed = True
125
126        isInList = False
127        #we just need aux to make the comparation, since componentClass is just a type and component is an instance of an object
128        aux = componentClass()
129        for component in self.__eventComponentsDict[event]:
130            if component.__class__ == aux.__class__:
131                isInList = True
132        #we don't want to have the 2 same components in the list, so we check it
133        if not isInList:
134            self.__eventComponentsDict[event].append(componentClass())
135            #we order the list according to each method's priority
136            self.__eventComponentsDict[event].sort(cmp=lambda x,y: cmp(x.getPriority(), y.getPriority()))
137            changed = True
138        if changed:
139            self._notifyModification()
140
141    def notifyComponent(self, event, obj, *params):
142        results = []
143        subscribers = self.getAllSubscribers(event)
144
145        for subscriber in subscribers:
146
147            f = getattr(subscriber,event)
148            try:
149                results.append(f(obj, *params))
150            except Exception, e:
151                Logger.get('ext.notification').exception("Exception while calling subscriber %s" % str(subscriber.__class__))
152                raise
153
154        if subscribers == []:
155            Logger.get('ext.notification').warning('Event %s not present in %s' %
156                                                   (event, obj))
157
158        return results
159
160    def getAllSubscribers(self, method):
161        if self.__eventComponentsDict.has_key(method):
162            return self.__eventComponentsDict[method]
163        else:
164            return []
165
166    def getAssociatedPlugin(self, component):
167        ''' returns the plugin the component belongs to, according to the component path'''
168
169        componentClassModuleName = component.__module__
170
171        for epoint in pkg_resources.iter_entry_points('indico.ext'):
172            if component.__module__.startswith(epoint.module_name):
173                moduleId = epoint.name.split('.')
174                return moduleId[0], moduleId[1]
175
176        for epoint in pkg_resources.iter_entry_points('indico.ext_types'):
177            if component.__module__.startswith(epoint.module_name):
178                return epoint.name, None
179        else:
180            raise Exception('Component %s does not belong to any plugin? %s ' % (component, list(ep.module_name for ep in pkg_resources.iter_entry_points('indico.ext'))))
181
182    def cleanPlugin(self, name):
183        '''receives the name of the plugin, (EVO, CERNMCU...) and unregisters it'''
184        #TODO: VERY inefficient!
185        changed = False
186        for event in self.__eventComponentsDict.keys():
187            #check if the component is in the dictionary to erase it
188            oldSize = len(self.__eventComponentsDict[event])
189            self.__eventComponentsDict[event] = list( component for component in self.__eventComponentsDict[event] if not self.pluginContainsComponent(name, component))
190            if oldSize != len(self.__eventComponentsDict[event]):
191                changed = True
192        if changed:
193            self._notifyModification()
194
195    def pluginContainsComponent(self, plugin, component):
196        associatedPlugin = self.getAssociatedPlugin(component)
197        if plugin == associatedPlugin[1]:
198            return True
199        return False
200
201    def addPlugin(self, name):
202        '''Someone activated a plugin (EVO, CERNMCU) and we need to register it again for the notifications'''
203
204        for component in self.__components:
205            if self.pluginContainsComponent(name, component):
206                self.registerComponent(component)
207
208    def cleanAll(self):
209        self.__eventComponentsDict.clear()
210        del self.__components[:]
211        self._notifyModification()
212
213    def _notifyModification(self):
214        self._p_changed = 1
215
216
217class AJAXMethodMap(Persistent):
218    def __init__(self):
219        self.__id = "ajaxMethodMap"
220        self.__map = {}
221
222    def _notifyModification(self):
223        self._p_changed = 1
224
225    def getAJAXMethodMap(self):
226        if not self.hasKey('ajaxMethodMap'):
227            self.add(AJAXMethodMap())
228        return self.getById('ajaxMethodMap')
229
230    def getId(self):
231        return self.__id
232
233    def addMethods2AJAXDict(self, dict):
234        """ Every time a handlers.py file is checked in the pluginLoader, it fills this dictionary with AJAX methods from its methodMap
235        """
236        self.__map.update(dict)
237        self._notifyModification()
238
239    def getAJAXDict(self):
240        return self.__map
241
242    def cleanAJAXDict(self):
243        """ Attributes in this class are persistent, so when we want to erase them we'll need to explicitely invoke this method
244        """
245        self.__map.clear()
246        self._notifyModification()
247
248
249
250class RHMap(Persistent):
251    """ This class is the representation in the DB of the RHMap """
252    def __init__(self):
253        self.__id = "RHMap"
254        self.__map = {}
255
256    def hasURL(self, rh):
257        if hasattr(rh, '_url') and rh._url != None:
258            return True
259        else:
260            return False
261
262    def has_key(self, key):
263        return self.__map.has_key(key)
264
265    def copy(self):
266        return self.__map.copy()
267
268    def _notifyModification(self):
269        self._p_changed = 1
270
271    def get(self):
272        return self.__map
273
274    def getId(self):
275        return self.__id
276
277    def addRH(self, rh):
278        if self.hasURL(rh):
279            self.__map[re.compile(rh._url)] = rh
280            self._notifyModification()
281
282    def cleanRHDict(self):
283        """ Attributes in this class are persistent, so when we want to erase them we'll need to explicitely invoke this method
284        """
285        self.__map.clear()
286        self._notifyModification()
287
288
289class PluginsHolder (ObjectHolder):
290    """ A PluginsHolder object is the "gateway" to all the methods for getting plugin meta-data stored in the DB.
291    """
292
293    idxName = "plugins"
294    counterName = "PLUGINS"
295
296    def __init__(self):
297        """ Creates / Returns a PluginsHolder object,
298            which is the "gateway" to all the methods for getting plugin meta-data.
299        """
300        ObjectHolder.__init__(self)
301        if not self.hasKey("globalPluginOptions"):
302            self.add(GlobalPluginOptions())
303        if not self.hasKey('componentsManager'):
304            self.add(ComponentsManager())
305        if len(self._getIdx()) == 0: #no plugins
306            self.loadAllPlugins()
307        if not self.hasKey('ajaxMethodMap'):
308            self.add(AJAXMethodMap())
309        if not self.hasKey('RHMap'):
310            self.add(RHMap())
311
312    def getRHMap(self):
313        if not self.hasKey('RHMap'):
314            self.add(RHMap())
315        return self.getById('RHMap')
316        # Replace with the real dict obtained while exploring the directories
317        #return {'algo': 'MaKaC.webinterface.rh.welcome.RHWelcome'}
318
319    def getGlobalPluginOptions(self):
320        """ Returns server-wide options relative to the whole plugin system.
321        """
322        return self.getById("globalPluginOptions")
323
324    def getComponentsManager(self):
325        if not self.hasKey('componentsManager'):
326            self.add(ComponentsManager())
327        return self.getById('componentsManager')
328
329    def loadAllPlugins(self):
330        """ Initially loads all plugins and stores their information
331        """
332        PluginLoader.loadPlugins()
333        self.updateAllPluginInfo()
334
335    def reloadAllPlugins(self):
336        """ Reloads all plugins and updates their information
337        """
338
339        self.getComponentsManager().cleanAll()
340        self.getById("ajaxMethodMap").cleanAJAXDict()
341        self.getById("RHMap").cleanRHDict()
342        PluginLoader.reloadPlugins()
343        self.updateAllPluginInfo()
344        self.getComponentsManager().registerAllComponents()
345
346    def reloadPluginType(self, pluginTypeName):
347        """ Reloads plugins of a given type and updates their information
348            pluginTypeName: a string such as "Collaboration"
349        """
350        PluginLoader.reloadPluginType(pluginTypeName)
351        if self.hasPluginType(pluginTypeName, mustBeActive=False):
352            self.getPluginType(pluginTypeName).updateInfo()
353        else:
354            raise PluginError("Error while trying to reload plugins of the type: " + pluginTypeName + ". Plugins of the type " + pluginTypeName + "do not exist")
355
356    def updateAllPluginInfo(self):
357        """ Updates the info about plugins in the DB
358            We must keep if plugins are active or not even between reloads
359            and even if plugins are removed from the file system (they may
360            be present again later)
361        """
362        for pt in self.getPluginTypes(includeNonPresent=True):
363            pt.setPresent(False)
364
365        ptypes = PluginLoader.getPluginTypeList()
366
367        for ptypeId in ptypes:
368
369            if self.hasPluginType(ptypeId, mustBePresent=False, mustBeActive=False):
370                ptype = self.getPluginType(ptypeId)
371                ptype.setPresent(True)
372            else:
373                ptype = PluginType(ptypeId)
374                self.add(ptype)
375
376            ptype.configureFromMetadata(processPluginMetadata(ptype.getModule()))
377            missingDeps = ptype.getModule().__missing_deps__
378
379            # if there are dependencies missing, set as not usable
380            if len(missingDeps) > 0:
381                ptype.setUsable(False, reason = "Dependencies missing: %s " % \
382                                missingDeps)
383                if ptype.isActive():
384                    ptype.setActive(False)
385            else:
386                ptype.setUsable(True)
387
388            ptype.updateInfo()
389
390    def clearPluginInfo(self):
391        """ Removes all the plugin information from the DB
392        """
393        for item in self.getValuesToList():
394            if isinstance(item, PluginType):
395                self.remove(item)
396        self._getTree("counters")[PluginsHolder.counterName] = Counter()
397
398    def getPluginTypes(self, doSort=False, includeNonPresent=False, includeNonVisible=True):
399        """ Returns a list of different PluginTypes (e.g. epayment, collaboration, roombooking)
400            doSort: if True, the list of PluginTypes will be sorted alphabetically
401            includeNonPresent: if True, non present PluginTypes will be included. A PluginType is present if it has a physical folder on
402            disk, inside MaKaC/plugins
403        """
404        pluginTypes = [pt for pt in self.getList() if isinstance(pt, PluginType) and
405                                                      (pt.isPresent() or includeNonPresent) and
406                                                      (pt.isVisible() or includeNonVisible)]
407        if doSort:
408            pluginTypes.sort(key=lambda pt: pt.getId())
409        return pluginTypes
410
411    def hasPluginType(self, name, mustBePresent=True, mustBeActive=True):
412        """
413        Returns True if there is a PluginType with the given name.
414        """
415        if self.hasKey(name):
416            pluginType = self.getById(name)
417            return (not mustBePresent or pluginType.isPresent()) and (not mustBeActive or pluginType.isActive())
418        else:
419            return False
420
421    def getPluginType(self, ptypeid):
422        """ Returns the PluginType object for the given name
423        """
424        return self.getById(ptypeid)
425
426
427
428class RHMapMemory:
429    """ Stores the RHMap for every python process in memory
430    If there's no Map attribute, we fetch it from the database,
431    otherwise just return it.
432    """
433
434    ## Stores the unique Singleton instance-
435    _iInstance = None
436
437    ## Class used with this Python singleton design pattern
438    #  @todo Add all variables, and methods needed for the Singleton class below
439    class Singleton:
440        def __init__(self):
441            if not hasattr(self, '_map'):
442                if DBMgr.getInstance().isConnected():
443                    self._map=PluginsHolder().getRHMap().copy()
444                else:
445                    DBMgr.getInstance().startRequest()
446                    self._map=PluginsHolder().getRHMap().copy()
447                    DBMgr.getInstance().endRequest()
448
449    ## The constructor
450    #  @param self The object pointer.
451    def __init__( self ):
452        # Check whether we already have an instance
453        if RHMapMemory._iInstance is None:
454            # Create and remember instance
455            RHMapMemory._iInstance = RHMapMemory.Singleton()
456
457        # Store instance reference as the only member in the handle
458        self._EventHandler_instance = RHMapMemory._iInstance
459
460    ## Delegate access to implementation.
461    #  @param self The object pointer.
462    #  @param attr Attribute wanted.
463    #  @return Attribute
464    def __getattr__(self, aAttr):
465        return getattr(self._iInstance, aAttr)
466
467
468    ## Delegate access to implementation.
469    #  @param self The object pointer.
470    #  @param attr Attribute wanted.
471    #  @param value Vaule to be set.
472    #  @return Result of operation.
473    def __setattr__(self, aAttr, aValue):
474        return setattr(self._iInstance, aAttr, aValue)
475
476
477class PluginBase(Persistent):
478    """ Base class for the Plugin and PluginType classes.
479        Both of these classes can store "options" and "actions" so the common logic is handled by this class.
480        It inherits from Persistent so that Plugin and PluginType don't have double inheritance.
481    """
482
483    def __init__(self):
484        """ Constructor.
485            The options are stored in a dictionary where the key is the option name and the value is a PluginOption object
486            (see the PluginOption class)
487        """
488        self.__options = {}
489        self.__actions = {}
490
491        self.__usable = False
492
493    ############## actions related ###############
494    @classmethod
495    def checkOptionAttributes(cls, name, attributes):
496        """ Utility method that takes a dictionary with option attributes.
497            It verifies some attributes and sets default values for others if they don't exist.
498        """
499        if attributes.has_key("description"):
500            description = attributes["description"]
501        else:
502            raise PluginError('Option ' + str(name) + ' does not have a "description" attribute')
503
504        if attributes.has_key("type"):
505            optionType = attributes["type"]
506        else:
507            raise PluginError('Option ' + str(name) + ' does not have a "type" attribute')
508
509        return {"description": description,
510                "type": optionType,
511                "subType": attributes.get("subType", None),
512                "defaultValue": attributes.get("defaultValue", None),
513                "editable": attributes.get("editable", True),
514                "visible": attributes.get("visible", True),
515                "mustReload": attributes.get("mustReload", False)
516                }
517
518
519    def updateAllOptions(self, retrievedPluginOptions):
520        """ Updated the attributes of the options of this Plugin / PluginType object.
521            retrievedPluginOptions can be a dictionary with the options or None if none were found
522        """
523        #we mark all the options as non present
524        for pto in self.getOptions().values():
525            pto.setPresent(False)
526
527        #we get the list of options of this type
528        if retrievedPluginOptions is not None:
529            for index, (name, attributes) in enumerate(retrievedPluginOptions):
530                if self.hasOption(name):
531                    self.updateOption(name, PluginBase.checkOptionAttributes(name, attributes), index)
532                else:
533                    self.addOption(name, PluginBase.checkOptionAttributes(name, attributes), index)
534
535    def getOptions(self, includeNonPresent=False, includeOnlyEditable=False,
536                   includeOnlyNonEditable=False, includeOnlyVisible=False,
537                   filterByType=None):
538        """ Returns all the options of this PluginType / Plugin, in dictionary form.
539            -if includeNonPresent == True, all the options are returned, otherwise, only the present ones
540            (present = declared in the relevant __init__.py file).
541            -if includeOnlyEditable = True, only the "editable" options are returned
542            -if includeOnlyNonEditable = True, only the non "editable" options are returned
543            -if includeOnlyVisible = True, only the "visible" options are returned
544            WARNING: this method returns a copy of the options, so use it only for reading, not for adding options.
545        """
546        return dict([(k, v) for k, v in self.__options.items()
547                     if (v.isPresent() or includeNonPresent) and
548                          ((v.isEditable() and not includeOnlyNonEditable) or (not v.isEditable() and not includeOnlyEditable)) and
549                          (v.isVisible() or not includeOnlyVisible) and
550                          (not filterByType or v.getType() == filterByType)
551                    ])
552
553    def getOptionList(self, doSort=False, includeNonPresent=False,
554                      includeOnlyEditable=False, includeOnlyNonEditable=False, includeOnlyVisible=False,
555                      filterByType=None):
556        """ Returns all the options of this PluginType / Plugin, in list form.
557            -if doSort = True, the options will be sorted by their order attribute.
558            -if includeNonPresent == True, all the options are returned, otherwise, only the present ones
559            (present = declared in the relevant __init__.py file).
560            -if includeOnlyEditable = True, only the "editable" options are returned
561            -if includeOnlyNonEditable = True, only the non "editable" options are returned
562            -if includeOnlyVisible = True, only the "visible" options are returned
563            WARNING: this method returns a copy of the options, so use it only for reading, not for writing values/ adding options.
564        """
565
566        options = self.getOptions(includeNonPresent, includeOnlyEditable, includeOnlyNonEditable, includeOnlyVisible, filterByType).values()
567        if doSort:
568            options.sort(key=lambda option: option.getOrder())
569        return options
570
571    def getOption(self, optionName):
572        """ Returns a PluginOption object given its name.
573        """
574        return self.__options[optionName]
575
576    def addOption(self, name, attributes, order):
577        """ Adds a new option to the set of options of this Plugin / PluginType object.
578            name: a string
579            attributes: a dictionary
580            order: an integer with the order of the options
581        """
582        self.__options[name] = PluginOption(name, attributes["description"], attributes["type"],
583                                            attributes["defaultValue"], attributes["editable"], attributes["visible"],
584                                            attributes["mustReload"], True, order, attributes["subType"])
585        self._notifyModification()
586
587    def updateOption(self, name, attributes, order):
588        """ Updates an existing option with new attribute values
589            name: a string
590            attributes: a dictionary
591            order: an integer with the order of the options
592        """
593        option = self.getOption(name)
594        option.setPresent(True)
595        option.setDescription(attributes["description"])
596        option.setType(attributes["type"])
597        option.setSubType(attributes["subType"])
598        option.setEditable(attributes["editable"])
599        option.setVisible(attributes["visible"])
600        option.setMustReload(attributes["mustReload"])
601        if option.isMustReload():
602            option.setValue(attributes["defaultValue"])
603        option.setOrder(order)
604
605    def hasOption(self, name):
606        """ Returns if this Plugin / PluginType object has an option given this name
607            name: a string with the name of the options
608        """
609        return name in self.__options
610
611    def hasAnyOptions(self):
612        """ Returns if this Plugin / PluginType object has any options at all
613        """
614        return len(self.getOptions()) > 0
615
616    def clearAssociatedActions(self):
617        """ Every option can have many associated actions. (an action is a button that will do something that changes the value of the option)
618            This utility method clears the actions for all options.
619        """
620        for option in self.__options.values():
621            option.clearActions()
622
623    ############## end of options related ###############
624
625
626    ############## actions related ###############
627    @classmethod
628    def checkActionAttributes(cls, name, attributes):
629        """ Utility method that takes a dictionary with actions attributes.
630            It verifies some attributes and sets default values for others if they don't exist.
631        """
632        if (not attributes.has_key("visible") or (attributes.has_key("visible") and attributes["visible"])) and not attributes.has_key("buttonText"):
633            raise PluginError('Action ' + str(name) + ' does not have a "buttonText" attribute, but it is visible')
634
635        return {"buttonText": attributes.get("buttonText", None),
636                "associatedOption": attributes.get("associatedOption", None),
637                "visible": attributes.get("visible", True),
638                "executeOnLoad": attributes.get("executeOnLoad", False),
639                "triggeredBy": attributes.get("triggeredBy", [])
640                }
641
642    def updateAllActions(self, retrievedPluginActions):
643        """ Updates information about the actions.
644            -retrievedPluginActions: a dictionary where
645            keys are strings (the action name) and values are 2-tuples of strings (buttonText, associatedOptionName)
646        """
647        self.__actions = {}
648        self.clearAssociatedActions()
649        #we get the list of actions of this type
650        if retrievedPluginActions is not None:
651            for index, (name, attributes) in enumerate(retrievedPluginActions):
652                checkedAttributes = PluginBase.checkActionAttributes(name, attributes)
653                associatedOptionName = checkedAttributes["associatedOption"]
654
655                if associatedOptionName is None:
656                    associatedOption = None
657                else:
658                    if self.hasOption(associatedOptionName):
659                        associatedOption = self.getOption(associatedOptionName)
660                    else:
661                        raise PluginError("action " + name + " of plugin " + self.getName() + " tried to associate with option " + associatedOptionName + " but this option doesn't exist")
662
663                newAction = self.addAction(name, checkedAttributes, index)
664
665                if associatedOption is not None:
666                    associatedOption.addAssociatedAction(self.getAction(name))
667
668                if newAction.isExecuteOnLoad():
669                    newAction.call()
670
671    def getActions(self, includeOnlyNonAssociated=False, includeOnlyTriggeredBy=False, includeOnlyVisible=True):
672        """ Returns all the actions of this Plugin, as a dictionary where
673            keys are strings (the action name) and values are PluginAction objects.
674        """
675        return dict([(k, v) for k, v in self.__actions.items()
676            if (not includeOnlyNonAssociated or not v.hasAssociatedOption()) and
677                (not includeOnlyTriggeredBy or v.hasTriggeredBy()) and
678                (not includeOnlyVisible or v.isVisible())
679        ])
680
681    def getActionList(self, doSort=False, includeOnlyNonAssociated=False, includeOnlyTriggeredBy=False, includeOnlyVisible=True):
682
683        actions = self.getActions(includeOnlyNonAssociated, includeOnlyTriggeredBy, includeOnlyVisible).values()
684        if doSort:
685            actions.sort(key=lambda action: action.getOrder())
686        return actions
687
688    def getAction(self, actionName):
689        return self.__actions[actionName]
690
691    def addAction(self, name, attributes, index):
692        newAction = PluginAction(name, self, attributes["buttonText"],
693                                 attributes["associatedOption"], attributes["visible"],
694                                 attributes["executeOnLoad"], attributes["triggeredBy"], index)
695        self.__actions[name] = newAction
696        self._notifyModification()
697        return newAction
698
699    def hasAction(self, name):
700        return self.__actions.has_key(name)
701
702    def hasAnyActions(self, includeOnlyNonAssociated=False):
703        return len(self.getActionList(includeOnlyNonAssociated=includeOnlyNonAssociated)) > 0
704    ############## end of actions related ###############
705
706    def _notifyModification(self):
707        self._p_changed = 1
708
709    def setUsable(self, value, reason = ''):
710        self.__notUsableReason = None if value else reason
711
712    def getNotUsableReason(self):
713        return self.__notUsableReason
714
715    def isUsable(self):
716        return self.__notUsableReason == None
717
718    def getStorage(self):
719        if not hasattr(self, "_storage") or self._storage is None:
720            self._storage = OOBTree()
721        return self._storage
722
723
724class PluginType (PluginBase):
725    """ This class represents a plugin type ("COllaboration", "epayment", etc.).
726        It will store information about the plugin type in the db.
727    """
728
729    def __init__(self, ptypeId, description=None):
730        """ Constructor
731            -name: a string with the type name. e.g. "Collaboration"
732            -description: a human readable description for this plugin type.
733            This class will store a list of plugins in the form of a dictionary. The keys will be the plugin names
734            and the values will be Plugin objects.
735            Also, it will have associated options, since it inherits from PluginBase.
736        """
737        PluginBase.__init__(self)
738        self.__id = ptypeId
739        self.__name = ptypeId
740        self.__description = description
741        self.__present = True
742        self.__plugins = {}
743        self._visible = True
744        self._active = False
745
746    def configureFromMetadata(self, metadata):
747        self.__name = metadata['name']
748
749    def _updatePluginInfo(self, pid, pluginModule, metadata):
750
751        if self.hasPlugin(pid):
752            p = self.getPlugin(pid)
753            p.setDescription(metadata['description'])
754            p.setPresent(True)
755
756        #if it didn't exist, we create it
757        else:
758            p = Plugin(pid,
759                       pluginModule.__name__,
760                       self,
761                       metadata['description'])
762
763            self.addPlugin(p)
764
765        p.configureFromMetadata(processPluginMetadata(p.getModule()))
766
767        missingDeps = p.getModule().__missing_deps__
768
769        if len(missingDeps) > 0:
770            p.setUsable(False, reason = "Dependencies missing: %s " % missingDeps)
771
772            if p.isActive():
773                p.setActive(False)
774
775        else:
776            p.setUsable(True)
777
778        if hasattr(pluginModule, "options") and \
779               hasattr(pluginModule.options, "globalOptions"):
780            p.updateAllOptions(pluginModule.options.globalOptions)
781
782        if hasattr(pluginModule, "actions") and \
783               hasattr(pluginModule.actions, "pluginActions"):
784            p.updateAllActions(pluginModule.actions.pluginActions)
785
786        self._updateComponentInfo(p, pluginModule)
787        self._updateHandlerInfo(p, pluginModule)
788        self._updateRHMapInfo(p, pluginModule)
789
790    def updateInfo(self):
791        """
792        Will update the information in the DB about this plugin type.
793        """
794
795        # until we detect them, the plugins of this given Type are marked as non-present
796        for plugin in self.getPluginList(includeNonPresent=True,
797                                         includeNonActive=True):
798            plugin.setPresent(False)
799
800        # we get the list of modules (plugins) of this type
801        pluginModules = PluginLoader.getPluginsByType(self.getId())
802
803        # we loop through the plugins
804        for pluginModule in pluginModules:
805            metadata = processPluginMetadata(pluginModule)
806
807            # skip ignored plugins
808            if metadata['ignore']:
809                continue
810            else:
811                self._updatePluginInfo(pluginModule.__plugin_id__,
812                                       pluginModule,
813                                       metadata)
814
815        ptypeModule = self.getModule()
816        ptypeMetadata = processPluginMetadata(ptypeModule)
817
818        # basic information
819        self.__name = ptypeMetadata['name']
820        self.__description = ptypeMetadata['description']
821        self.__visible = ptypeMetadata['visible']
822
823        # components, handlers, options and actions
824        self._updateComponentInfo(self, ptypeModule)
825        self._updateRHMapInfo(self, ptypeModule)
826        self._updateHandlerInfo(self, ptypeModule)
827        self.updateAllOptions(self._retrievePluginTypeOptions())
828        self.updateAllActions(self._retrievePluginTypeActions())
829
830
831    def _getAllSubmodules(self, module):
832        accum = []
833
834        for obj in module.__dict__.itervalues():
835
836            # check if it's a module and it is inside the parent module
837            # ignore test modules
838            if type(obj) == types.ModuleType and \
839                   obj.__name__.startswith(module.__name__) and \
840                   obj.__name__.split('.')[-1] != 'test':
841                    accum += [obj]
842                    accum += self._getAllSubmodules(obj)
843
844        return accum
845
846    def _updateComponentInfo(self, plugin, module):
847        Logger.get('plugins.holder').info("Updating component info for '%s'" % \
848                                          plugin.getFullId())
849
850        for smodule in self._getAllSubmodules(module):
851            for obj in smodule.__dict__.values():
852                if type(obj) == type and Component in obj.mro() and \
853                   (IListener.implementedBy(obj) or IContributor.implementedBy(obj)):
854                    Logger.get('plugins.holder.component').debug(
855                        "Registering component %s" % obj)
856                    PluginsHolder().getComponentsManager().addComponent(obj)
857
858    def _updateRHMapInfo(self, plugin, module):
859        from MaKaC.webinterface.rh.base import RH
860        for smodule in self._getAllSubmodules(module):
861            Logger.get('plugins.holder.rhmap').debug(
862                "Analyzing %s" % smodule)
863            for obj in smodule.__dict__.values():
864                # account for old style and new style class/rh
865                if (type(obj) == types.ClassType and RH in inspect.getmro(obj)) or \
866                   (type(obj) == type and newrh.RH in obj.mro()):
867                    PluginsHolder().getRHMap().addRH(obj)
868
869    def _updateHandlerInfo(self, plugin, module):
870        """
871        """
872
873        # TODO: Maybe let handlers be defined anywhere?
874
875        # By default, use module.handlers.methodMap
876        if hasattr(module, 'handlers') and \
877           isinstance(module.handlers, types.ModuleType):
878            if hasattr(module.handlers, 'methodMap') and \
879               type(module.handlers.methodMap) == dict:
880                PluginsHolder().getById('ajaxMethodMap').addMethods2AJAXDict(
881                    module.handlers.methodMap)
882            else:
883                # in case there's no methodMap, skip it and leave a warning
884                Logger.get('plugins.holder').warning("%s has no methodMap? "
885                                                     "(or is it invalid?)" % \
886                                                     module.__name__)
887
888    def _retrievePluginTypeOptions(self):
889
890        hasOptionsModule = hasattr(self.getModule(), "options")
891        hasGlobalOptionsVariable = hasOptionsModule and hasattr(self.getModule().options, "globalOptions")
892        if hasOptionsModule and hasGlobalOptionsVariable:
893            return self.getModule().options.globalOptions
894        else:
895            return None
896
897    def _retrievePluginTypeActions(self):
898        hasActionsModule = hasattr(self.getModule(), "actions")
899        hasPluginTypeActions = hasActionsModule and hasattr(self.getModule().actions, "pluginTypeActions")
900        if hasActionsModule and hasPluginTypeActions:
901            return self.getModule().actions.pluginTypeActions
902        else:
903            return None
904
905    def getId(self):
906        return self.__id
907
908    def getFullId(self):
909        return self.__id
910
911    def setId(self, id):
912        self.__id = id
913
914    def getName(self):
915        return self.__name if hasattr(self, '_PluginType__name') and self.__name is not None else self.__id
916
917    def getDescription(self):
918        return self.__description
919
920    def hasDescription(self):
921        return (self.__description is not None) and len(self.__description) > 0
922
923    def isPresent(self):
924        return self.__present
925
926    def setPresent(self, present):
927        self.__present = present
928
929    def getPlugin(self, id):
930        return self.__plugins[id]
931
932    def hasPlugin(self, id):
933        return id in self.__plugins
934
935    def addPlugin(self, plugin):
936        self.__plugins[plugin.getId()] = plugin
937        self._notifyModification()
938
939    def getModule(self):
940        return PluginLoader.getPluginType(self.getId())
941
942    def hasPlugins(self):
943        """ Returns if this plugin type has any plugins at all.
944        """
945        return len(self.__plugins) > 0
946
947    def getPlugins(self, includeNonPresent=False, includeTestPlugins=False, includeNonActive=False, filterFunction=lambda plugin: True):
948        """ Returns a dictionary with the plugins of this PluginType.
949            They keys of the dictionary will be strings, with the name of each plugin (e.g. "EVO"). The values will be Plugin objects.
950            -includeNonPresent: if True, non present plugins (i.e., plugins in the DB but no longer in the file system) will be returned
951            -includeNonActive: if True, non active plugins will be returned
952            -filterFunction: a function that will be passed a plugin as argument and returns if the plugin should be returned or not.
953        """
954        return dict([(k, v) for k, v in self.__plugins.items()
955                     if (v.isPresent() or includeNonPresent) and (not v.isTestPlugin() or includeTestPlugins) and (v.isActive() or includeNonActive) and filterFunction(v)])
956
957    def getPluginList(self, doSort=False, includeNonPresent=False, includeTestPlugins=False, includeNonActive=False, filterFunction=lambda plugin: True):
958        """ Returns a list with the plugins of this PluginType, as a list of Plugin objects.
959            -doSort: if True, the list will be sorted alphabetically by plugin name.
960            -includeNonPresent: if True, non present plugins (i.e., plugins in the DB but no longer in the file system) will be returned
961            -includeNonActive: if True, non active plugins will be returned
962            -filterFunction: a function that will be passed a plugin as argument and returns if the plugin should be returned or not.
963        """
964        plugins = self.getPlugins(includeNonPresent, includeTestPlugins, includeNonActive, filterFunction)
965        if doSort:
966            keys = plugins.keys()
967            keys.sort()
968            return [plugins[k] for k in keys]
969        else:
970            return plugins.values()
971
972    def getLocator(self):
973        l = Locator()
974        l["pluginType"] = self.getId()
975        return l
976
977    def isVisible(self):
978        if not hasattr(self, "_visible"):
979            self._visible = True
980        return self._visible
981
982    def isActive(self):
983        if not hasattr(self, "_active"):
984            self._active = False
985        return self._active
986
987    def setActive(self, value):
988        self._active = value
989        PluginsHolder().reloadAllPlugins()
990
991    def toggleActive(self):
992        if not self.isActive():
993            self._active = True
994            #register the components related to the plugin
995            PluginsHolder().reloadAllPlugins()
996            #PluginsHolder().getComponentsManager().addPluginType(self.getName(), True)
997        else:
998            self._active = False
999            #unregister the components related to the plugin
1000            PluginsHolder().reloadAllPlugins()
1001            #PluginsHolder().getComponentsManager().cleanPluginType(self.getName(), True)
1002
1003
1004class Plugin(PluginBase):
1005    """ This class represents a plugin ("EVO", "paypal", etc.).
1006        It will store information about the plugin in the db.
1007    """
1008
1009
1010    def __init__(self, pid, moduleName, owner, description=None, active=False):
1011        """ Constructor
1012            -moduleName: the module name corresponding to this plugin. e.g. "MaKaC.plugins.Collaboration.EVO"
1013            -owner : the PluginType of this Plugin
1014            -description: a human readable description of the purpose of this plugin
1015            -active: a boolean that stores if this plugin is active or not. If not active, it shouldn't exist for the rest of Indico.
1016
1017            A plugin object will have associated options that can be set up from the Server Admin interface.
1018            Also it will have "actions". For example, the EVO plugin has a "reload communities" action which will
1019            contact the EVO server and change the value of the "community list" option. The list of communities
1020            needs to be used later when interacting with the Indico user. Information about the actions will be stored in
1021            the DB in a similar way to options.
1022        """
1023        PluginBase.__init__(self)
1024        self.__name = pid
1025        self.__id = pid.replace(' ','')
1026        self.__owner = owner
1027        self.__present = True
1028        self.__moduleName = moduleName
1029        self.__description = description
1030        self.__active = active
1031        self._testPlugin = False
1032        self._globalData = self.initializeGlobalData()
1033        self._storage = OOBTree() # storage.....
1034
1035    def configureFromMetadata(self, metadata):
1036        self.__name = metadata['name']
1037        self._testPlugin = metadata['testPlugin']
1038
1039    def getId(self):
1040        return self.__id
1041
1042    def setId(self, newId):
1043        self.__id = newId
1044
1045    def getFullId(self):
1046        return "%s.%s" % (self.getOwner().getId(), self.__id)
1047
1048    def getName(self):
1049        return self.__name
1050
1051    def getOwner(self):
1052        return self.__owner
1053
1054    def isPresent(self):
1055        return self.__present
1056
1057    def setPresent(self, present):
1058        self.__present = present
1059
1060    def getModuleName(self):
1061        return self.__moduleName
1062
1063    def getModule(self):
1064        return PluginLoader.getPluginByTypeAndId(self.getType(), self.getId())
1065
1066    def getType(self):
1067        return self.getOwner().getId()
1068
1069    def hasDescription(self):
1070        return (self.__description is not None) and len(self.__description) > 0
1071
1072    def getDescription(self):
1073        return self.__description
1074
1075    def setDescription(self, description):
1076        self.__description = description
1077
1078    def isActive(self):
1079        return self.__active
1080
1081    def setActive(self, value):
1082        self.__active = value
1083        if value:
1084            # register the components related to the plugin
1085            PluginsHolder().getComponentsManager().addPlugin(self.getName())
1086        else:
1087            # unregister the components related to the plugin
1088            PluginsHolder().getComponentsManager().cleanPlugin(self.getName())
1089
1090    def toggleActive(self):
1091        self.setActive(not self.isActive())
1092
1093    def isTestPlugin(self):
1094        if not hasattr(self, "_testPlugin"): #TODO: remove when safe
1095            self._testPlugin = False
1096        return self._testPlugin
1097
1098    def initializeGlobalData(self):
1099        module = self.getModule()
1100        if hasattr(module, 'common') and hasattr(module.common, 'GlobalData'):
1101            return module.common.GlobalData()
1102        else:
1103            return None
1104
1105    def getGlobalData(self):
1106        if not hasattr(self, "_globalData") or self._globalData is None:
1107            self._globalData = self.initializeGlobalData()
1108        return self._globalData
1109
1110    def setGlobalData(self, globalData):
1111        self._globalData = globalData
1112
1113    def getLocator(self):
1114        l = self.__owner.getLocator()
1115        l["pluginId"] = self.getId()
1116        return l
1117
1118    def __str__(self):
1119        return str(self.__name)
1120
1121
1122class PluginOption(Persistent):
1123    """ Represents an option of a PluginType or a Plugin object.
1124        name: the id / name of the option
1125        description: a human-readable description
1126        type: the type of the value of the option: str, int, list, dict ...
1127        value: the initial value of the option, if given
1128        editable: says if this option's value will be editable in the plugin admin interface
1129        visible: says if this option's value will be visible in the plugin admin interface
1130        mustReload: says if this option's value should be set to the default value when the options are reloaded.
1131                    this is the only way to change values of a non-editable option without accessing the DB directly
1132        present: this will be false if the option is still in the DB, but not anymore in the __init__.py file of the plugin
1133        order: an integer so that the options can be displayed in a given order (ascending order)
1134    """
1135    _extraTypes = {
1136        'users': list,
1137        'usersGroups': list,
1138        'rooms': list,
1139        'password': str,
1140        'ckEditor': str,
1141        'list_multiline': list,
1142        'links': list
1143    }
1144
1145    def __init__(self, name, description, valueType, value=None, editable=True, visible=True, mustReload=False, present=True, order=0, subType=None):
1146        self.__name = name
1147        self.__description = description
1148        self.__type = valueType
1149        self.__subType = subType
1150        self.__present = present
1151        self.__editable = editable
1152        self.__visible = visible
1153        self.__mustReload = mustReload
1154        if value is None:
1155            self.__value = None
1156        else:
1157            self.setValue(value)
1158        self.__associatedActions = []
1159        self.__order = order
1160
1161    def getName(self):
1162        return self.__name
1163
1164    def getDescription(self):
1165        return self.__description
1166
1167    def getType(self):
1168        return self.__type
1169
1170    def getSubType(self):
1171        return self.__subType
1172
1173    def getValue(self):
1174        return self.__value
1175
1176    def setName(self, value):
1177        self.__name = value
1178
1179    def setDescription(self, description):
1180        self.__description = description
1181
1182    def isPresent(self):
1183        return self.__present
1184
1185    def setPresent(self, present):
1186        self.__present = present
1187
1188    def isEditable(self):
1189        return self.__editable
1190
1191    def setEditable(self, editable):
1192        self.__editable = editable
1193
1194    def isVisible(self):
1195        return self.__visible
1196
1197    def setVisible(self, visible):
1198        self.__visible = visible
1199
1200    def isMustReload(self):
1201        try:
1202            return self.__mustReload
1203        except AttributeError:
1204            self.__mustReload = False
1205            return self.__mustReload
1206
1207    def setMustReload(self, mustReload):
1208        self.__mustReload = mustReload
1209
1210    def setType(self, valueType):
1211        self.__type = valueType
1212
1213    def setSubType(self, subType):
1214        self.__subType = subType
1215
1216    def setValue(self, value):
1217        if isinstance(self.__type, type):
1218            self.__value = self.__type(value)
1219        elif self.__type in PluginOption._extraTypes:
1220            self.__value = PluginOption._extraTypes[self.__type](value)
1221        else:
1222            raise PluginError("""Tried to set value of option %s with type %s but this type is not recognized""" % (self.__name, self.__type))
1223
1224    def addAssociatedAction(self, action):
1225        self.__associatedActions.append(action)
1226
1227    def hasActions(self):
1228        return len(self.__associatedActions) > 0
1229
1230    def getActions(self):
1231        return self.__associatedActions
1232
1233    def clearActions(self):
1234        self.__associatedActions = []
1235
1236    def getOrder(self):
1237        if not hasattr(self, "_PluginOption__order"): #TODO: remove when safe
1238            self.__order = 0
1239        return self.__order
1240
1241    def setOrder(self, order):
1242        self.__order = order
1243
1244    def _notifyModification(self):
1245        self._p_changed = 1
1246
1247
1248class PluginAction(Persistent):
1249    """ Represents an "action" of a Plugin object.
1250        An "action" is a button that can be pressed to trigger a function call
1251        Its name is the function inside the actions.py file of each plugin that will be called.
1252    """
1253
1254    def __init__(self, name, owner, buttonText, associatedOption=None, visible=True, executeOnLoad=False, triggeredBy=[], order=0):
1255        """ Constructor for the PluginAction class
1256            name: the name of the action. It's also the name of the function inside the actions.py file of each plugin that will be called.
1257            owner: the Plugin or PluginType object that owns this action
1258            buttonText: a string that will be used in the button to trigger this action
1259            associatedOption: the name of a PluginOption of this same plugin. It implies that this action will change the value of that option.
1260                              thus, its button should appear next to the value of that option.
1261            visible: if True, a button to execute the action will appear
1262            executeOnLoad: if True, the action will be executing when loading / reloading the plugins
1263            triggeredBy: a list of PluginOption names of the same Plugin or child Plugins (if it's a PluginType)
1264                         if not an empty list, this action will be executed when one of those options is saved.
1265            order: an integer so that the actions can be displayed in a given order (ascending order)
1266        """
1267        self.__name = name
1268        self.__owner = owner
1269        self.__buttonText = buttonText
1270        self.__associatedOption = associatedOption
1271        self.__visible = visible
1272        self.__executeOnLoad = executeOnLoad
1273        self.__triggeredBy = triggeredBy
1274        self.__order = order
1275
1276    def getName(self):
1277        return self.__name
1278
1279    def getOwner(self):
1280        return self.__owner
1281
1282    def getButtonText(self):
1283        return self.__buttonText
1284
1285    def hasAssociatedOption(self):
1286        return self.__associatedOption is not None
1287
1288    def getAssociatedOption(self):
1289        return self.__associatedOption
1290
1291    def setName(self, name):
1292        self.__name = name
1293
1294    def setButtonText(self, buttonText):
1295        self.__buttonText = buttonText
1296
1297    def setAssociatedOption(self, associatedOption):
1298        self.__associatedOption = associatedOption
1299
1300    def isVisible(self):
1301        return self.__visible
1302
1303    def setVisible(self, visible):
1304        self.__visible = visible
1305
1306    def isExecuteOnLoad(self):
1307        return self.__executeOnLoad
1308
1309    def setExecuteOnLoad(self, executeOnLoad):
1310        self.__executeOnLoad = executeOnLoad
1311
1312    def hasTriggeredBy(self):
1313        return len(self.__triggeredBy) > 0
1314
1315    def getTriggeredBy(self):
1316        return self.__triggeredBy
1317
1318    def setTriggeredBy(self, triggeredBy):
1319        self.__triggeredBy = triggeredBy
1320
1321    def getOrder(self):
1322        if not hasattr(self, "_PluginAction__order"): #TODO: remove when safe
1323            self.__order = 0
1324        return self.__order
1325
1326    def setOrder(self, order):
1327        self.__order = order
1328
1329    def call(self):
1330        actionClassName = self.__name[0].upper() + self.__name[1:] + "Action"
1331        clazz = getattr(self.__owner.getModule().actions, actionClassName)
1332        return clazz(self).call()
1333
1334
1335class ActionBase(object):
1336    """ Base class for plugins to implement their actions.
1337        The "call" method will be called when a button representing an action is pushed.
1338    """
1339
1340    def __init__(self, pluginAction):
1341        self._pluginAction = pluginAction
1342        if isinstance(pluginAction.getOwner(), Plugin):
1343            self._plugin = pluginAction.getOwner()
1344            self._pluginType = self._plugin.getOwner()
1345        else:
1346            self._pluginType = pluginAction.getOwner()
1347
1348    def call(self):
1349        """ To be implemented by inheriting classes
1350        """
1351        raise PluginError("Action of class " + str(self.__class__.__name__) + " has not implemented the method call()")
1352
Note: See TracBrowser for help on using the repository browser.