cwconfig.py
changeset 0 b97547f5f1fa
child 140 478bdd15bc0e
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """common configuration utilities for cubicweb
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 import sys
       
    10 import os
       
    11 import logging
       
    12 from os.path import exists, join, expanduser, abspath, basename
       
    13 
       
    14 from logilab.common.decorators import cached
       
    15 from logilab.common.configuration import (Configuration, Method,
       
    16                                           ConfigurationMixIn, merge_options)
       
    17 
       
    18 from cubicweb import CW_SOFTWARE_ROOT, CW_MIGRATION_MAP, ConfigurationError
       
    19 from cubicweb.toolsutils import env_path, read_config, create_dir
       
    20 
       
    21 CONFIGURATIONS = []
       
    22 
       
    23 _ = unicode
       
    24 
       
    25 class metaconfiguration(type):
       
    26     """metaclass to automaticaly register configuration"""
       
    27     def __new__(mcs, name, bases, classdict):
       
    28         cls = super(metaconfiguration, mcs).__new__(mcs, name, bases, classdict)
       
    29         if classdict.get('name'):
       
    30             CONFIGURATIONS.append(cls)
       
    31         return cls
       
    32 
       
    33 def configuration_cls(name):
       
    34     """return the configuration class registered with the given name"""
       
    35     try:
       
    36         return [c for c in CONFIGURATIONS if c.name == name][0]
       
    37     except IndexError:
       
    38         raise ConfigurationError('no such config %r (check it exists with "cubicweb-ctl list")' % name)
       
    39 
       
    40 def possible_configurations(directory):
       
    41     """return a list of installed configurations in a directory
       
    42     according to *-ctl files
       
    43     """
       
    44     return [name for name in ('repository', 'twisted', 'all-in-one')
       
    45             if exists(join(directory, '%s.conf' % name))]
       
    46 
       
    47 def guess_configuration(directory):
       
    48     """try to guess the configuration to use for a directory. If multiple
       
    49     configurations are found, ConfigurationError is raised
       
    50     """
       
    51     modes = possible_configurations(directory)
       
    52     if len(modes) != 1:
       
    53         raise ConfigurationError('unable to guess configuration from %r %s'
       
    54                                  % (directory, modes))
       
    55     return modes[0]
       
    56 
       
    57 # XXX generate this according to the configuration (repository/all-in-one/web)
       
    58 VREGOPTIONS = []
       
    59 for registry in ('etypes', 'hooks', 'controllers', 'actions', 'components',
       
    60                  'views', 'templates', 'boxes', 'contentnavigation', 'urlrewriting',
       
    61                  'facets'):
       
    62     VREGOPTIONS.append(('disable-%s'%registry,
       
    63                         {'type' : 'csv', 'default': (),
       
    64                          'help': 'list of identifier of application objects from the %s registry to disable'%registry,
       
    65                          'group': 'appobjects', 'inputlevel': 2,
       
    66                          }))
       
    67 VREGOPTIONS = tuple(VREGOPTIONS)
       
    68 
       
    69 # persistent options definition
       
    70 PERSISTENT_OPTIONS = (
       
    71     ('encoding',
       
    72      {'type' : 'string',
       
    73       'default': 'UTF-8',
       
    74       'help': _('user interface encoding'),
       
    75       'group': 'ui', 'sitewide': True,
       
    76       }),    
       
    77     ('language',
       
    78      {'type' : 'string',
       
    79       'default': 'en',
       
    80       'vocabulary': Method('available_languages'),
       
    81       'help': _('language of the user interface'),
       
    82       'group': 'ui', 
       
    83       }),
       
    84     ('date-format',
       
    85      {'type' : 'string',
       
    86       'default': '%Y/%m/%d',
       
    87       'help': _('how to format date in the ui ("man strftime" for format description)'),
       
    88       'group': 'ui', 
       
    89       }),
       
    90     ('datetime-format',
       
    91      {'type' : 'string',
       
    92       'default': '%Y/%m/%d %H:%M',
       
    93       'help': _('how to format date and time in the ui ("man strftime" for format description)'),
       
    94       'group': 'ui', 
       
    95       }),
       
    96     ('time-format',
       
    97      {'type' : 'string',
       
    98       'default': '%H:%M',
       
    99       'help': _('how to format time in the ui ("man strftime" for format description)'),
       
   100       'group': 'ui', 
       
   101       }),
       
   102     ('float-format',
       
   103      {'type' : 'string',
       
   104       'default': '%.3f',
       
   105       'help': _('how to format float numbers in the ui'),
       
   106       'group': 'ui', 
       
   107       }),
       
   108     ('default-text-format',
       
   109      {'type' : 'choice',
       
   110       'choices': ('text/plain', 'text/rest', 'text/html'),
       
   111       'default': 'text/html', # use fckeditor in the web ui
       
   112       'help': _('default text format for rich text fields.'),
       
   113       'group': 'ui', 
       
   114       }),
       
   115     ('short-line-size',
       
   116      {'type' : 'int',
       
   117       'default': 40,
       
   118       'help': _('maximum number of characters in short description'),
       
   119       'group': 'navigation',
       
   120       }),
       
   121     )
       
   122 
       
   123 def register_persistent_options(options):
       
   124     global PERSISTENT_OPTIONS
       
   125     PERSISTENT_OPTIONS = merge_options(PERSISTENT_OPTIONS + options)
       
   126                 
       
   127 CFGTYPE2ETYPE_MAP = {
       
   128     'string': 'String',
       
   129     'choice': 'String',
       
   130     'yn':     'Boolean',
       
   131     'int':    'Int',
       
   132     'float' : 'Float',
       
   133     }
       
   134     
       
   135 class CubicWebNoAppConfiguration(ConfigurationMixIn):
       
   136     """base class for cubicweb configuration without a specific instance directory
       
   137     """
       
   138     __metaclass__ = metaconfiguration
       
   139     # to set in concrete configuration
       
   140     name = None
       
   141     # log messages format (see logging module documentation for available keys)
       
   142     log_format = '%(asctime)s - (%(name)s) %(levelname)s: %(message)s'
       
   143     # nor remove vobjects based on unused interface
       
   144     cleanup_interface_sobjects = True
       
   145 
       
   146     if os.environ.get('APYCOT_ROOT'):
       
   147         mode = 'test'
       
   148         CUBES_DIR = '%(APYCOT_ROOT)s/local/share/cubicweb/cubes/' % os.environ
       
   149     elif exists(join(CW_SOFTWARE_ROOT, '.hg')):
       
   150         mode = 'dev'
       
   151         CUBES_DIR = join(CW_SOFTWARE_ROOT, '../cubes')
       
   152     else:
       
   153         mode = 'installed'
       
   154         CUBES_DIR = '/usr/share/cubicweb/cubes/'
       
   155 
       
   156     options = VREGOPTIONS + (
       
   157        ('log-threshold',
       
   158          {'type' : 'string', # XXX use a dedicated type?
       
   159           'default': 'ERROR',
       
   160           'help': 'server\'s log level',
       
   161           'group': 'main', 'inputlevel': 1,
       
   162           }),
       
   163         # pyro name server
       
   164         ('pyro-ns-host',
       
   165          {'type' : 'string',
       
   166           'default': '',
       
   167           'help': 'Pyro name server\'s host. If not set, will be detected by a \
       
   168 broadcast query',
       
   169           'group': 'pyro-name-server', 'inputlevel': 1,
       
   170           }),
       
   171         ('pyro-ns-port',
       
   172          {'type' : 'int',
       
   173           'default': None,
       
   174           'help': 'Pyro name server\'s listening port. If not set, default \
       
   175 port will be used.',
       
   176           'group': 'pyro-name-server', 'inputlevel': 1,
       
   177           }),
       
   178         ('pyro-ns-group',
       
   179          {'type' : 'string',
       
   180           'default': 'cubicweb',
       
   181           'help': 'Pyro name server\'s group where the repository will be \
       
   182 registered.',
       
   183           'group': 'pyro-name-server', 'inputlevel': 1,
       
   184           }),
       
   185         # common configuration options which are potentially required as soon as
       
   186         # you're using "base" application objects (ie to really server/web
       
   187         # specific)
       
   188         ('base-url',
       
   189          {'type' : 'string',
       
   190           'default': None,
       
   191           'help': 'web server root url',
       
   192           'group': 'main', 'inputlevel': 1,
       
   193           }),
       
   194         ('mangle-emails',
       
   195          {'type' : 'yn',
       
   196           'default': False,
       
   197           'help': "don't display actual email addresses but mangle them if \
       
   198 this option is set to yes",
       
   199           'group': 'email', 'inputlevel': 2,
       
   200           }),
       
   201         )
       
   202     # static and class methods used to get application independant resources ##
       
   203         
       
   204     @staticmethod
       
   205     def cubicweb_version():
       
   206         """return installed cubicweb version"""
       
   207         from logilab.common.changelog import Version
       
   208         from cubicweb import __pkginfo__
       
   209         version = __pkginfo__.numversion
       
   210         assert len(version) == 3, version
       
   211         return Version(version)
       
   212     
       
   213     @staticmethod
       
   214     def persistent_options_configuration():
       
   215         return Configuration(options=PERSISTENT_OPTIONS)
       
   216 
       
   217     @classmethod
       
   218     def shared_dir(cls):
       
   219         """return the shared data directory (i.e. directory where standard
       
   220         library views and data may be found)
       
   221         """
       
   222         if cls.mode in ('dev', 'test') and not os.environ.get('APYCOT_ROOT'):
       
   223             return join(CW_SOFTWARE_ROOT, 'web')
       
   224         return join(cls.cubes_dir(), 'shared')
       
   225         
       
   226     @classmethod
       
   227     def i18n_lib_dir(cls):
       
   228         """return application's i18n directory"""
       
   229         if cls.mode in ('dev', 'test') and not os.environ.get('APYCOT_ROOT'):
       
   230             return join(CW_SOFTWARE_ROOT, 'i18n')
       
   231         return join(cls.shared_dir(), 'i18n')
       
   232 
       
   233     @classmethod
       
   234     def available_cubes(cls):
       
   235         cubes_dir = cls.cubes_dir()
       
   236         return sorted(cube for cube in os.listdir(cubes_dir)
       
   237                       if os.path.isdir(os.path.join(cubes_dir, cube))
       
   238                       and not cube in ('CVS', '.svn', 'shared', '.hg'))
       
   239     
       
   240     @classmethod
       
   241     def cubes_dir(cls):
       
   242         """return the application cubes directory"""
       
   243         return env_path('CW_CUBES', cls.CUBES_DIR, 'cubes')
       
   244     
       
   245     @classmethod
       
   246     def cube_dir(cls, cube):
       
   247         """return the cube directory for the given cube id,
       
   248         raise ConfigurationError if it doesn't exists
       
   249         """
       
   250         cube_dir = join(cls.cubes_dir(), cube)
       
   251         if not exists(cube_dir):
       
   252             raise ConfigurationError('no cube %s in %s' % (
       
   253                 cube, cls.cubes_dir()))
       
   254         return cube_dir
       
   255 
       
   256     @classmethod
       
   257     def cube_migration_scripts_dir(cls, cube):
       
   258         """cube migration scripts directory"""
       
   259         return join(cls.cube_dir(cube), 'migration')
       
   260     
       
   261     @classmethod
       
   262     def cube_pkginfo(cls, cube):
       
   263         """return the information module for the given cube"""
       
   264         cube = CW_MIGRATION_MAP.get(cube, cube)
       
   265         try:
       
   266             return getattr(__import__('cubes.%s.__pkginfo__' % cube), cube).__pkginfo__
       
   267         except ImportError:
       
   268             raise ConfigurationError('unable to find packaging information for '
       
   269                                      'cube %s' % cube)
       
   270 
       
   271     @classmethod
       
   272     def cube_version(cls, cube):
       
   273         """return the version of the cube located in the given directory        
       
   274         """
       
   275         from logilab.common.changelog import Version
       
   276         version = cls.cube_pkginfo(cube).numversion
       
   277         assert len(version) == 3, version
       
   278         return Version(version)
       
   279 
       
   280     @classmethod
       
   281     def cube_dependencies(cls, cube):
       
   282         """return cubicweb cubes used by the given cube"""
       
   283         return getattr(cls.cube_pkginfo(cube), '__use__', ())
       
   284 
       
   285     @classmethod
       
   286     def cube_recommends(cls, cube):
       
   287         """return cubicweb cubes recommended by the given cube"""
       
   288         return getattr(cls.cube_pkginfo(cube), '__recommend__', ())
       
   289 
       
   290     @classmethod
       
   291     def expand_cubes(cls, cubes):
       
   292         """expand the given list of top level cubes used by adding recursivly
       
   293         each cube dependencies
       
   294         """
       
   295         cubes = list(cubes)
       
   296         todo = cubes[:]
       
   297         while todo:
       
   298             cube = todo.pop(0)
       
   299             for depcube in cls.cube_dependencies(cube):
       
   300                 if depcube not in cubes:
       
   301                     depcube = CW_MIGRATION_MAP.get(depcube, depcube)
       
   302                     cubes.append(depcube)
       
   303                     todo.append(depcube)
       
   304         return cubes
       
   305 
       
   306     @classmethod
       
   307     def reorder_cubes(cls, cubes):
       
   308         """reorder cubes from the top level cubes to inner dependencies
       
   309         cubes
       
   310         """
       
   311         from logilab.common.graph import get_cycles
       
   312         graph = {}
       
   313         for cube in cubes:
       
   314             cube = CW_MIGRATION_MAP.get(cube, cube)
       
   315             deps = cls.cube_dependencies(cube) + \
       
   316                    cls.cube_recommends(cube)
       
   317             graph[cube] = set(dep for dep in deps if dep in cubes)
       
   318         cycles = get_cycles(graph)
       
   319         if cycles:
       
   320             cycles = '\n'.join(' -> '.join(cycle) for cycle in cycles)
       
   321             raise ConfigurationError('cycles in cubes dependencies: %s'
       
   322                                      % cycles)
       
   323         cubes = []
       
   324         while graph:
       
   325             # sorted to get predictable results
       
   326             for cube, deps in sorted(graph.items()):
       
   327                 if not deps:
       
   328                     cubes.append(cube)
       
   329                     del graph[cube]
       
   330                     for deps in graph.itervalues():
       
   331                         try:
       
   332                             deps.remove(cube)
       
   333                         except KeyError:
       
   334                             continue
       
   335         return tuple(reversed(cubes))
       
   336     
       
   337     @classmethod
       
   338     def cls_adjust_sys_path(cls):
       
   339         """update python path if necessary"""
       
   340         try:
       
   341             templdir = abspath(join(cls.cubes_dir(), '..'))
       
   342             if not templdir in sys.path:
       
   343                 sys.path.insert(0, templdir)
       
   344         except ConfigurationError:
       
   345             return # cube dir doesn't exists
       
   346 
       
   347     @classmethod
       
   348     def load_cwctl_plugins(cls):
       
   349         from logilab.common.modutils import load_module_from_file
       
   350         cls.cls_adjust_sys_path()
       
   351         for ctlfile in ('web/webctl.py',  'etwist/twctl.py',
       
   352                         'server/serverctl.py', 'hercule.py',
       
   353                         'devtools/devctl.py', 'goa/goactl.py'):
       
   354             if exists(join(CW_SOFTWARE_ROOT, ctlfile)):
       
   355                 load_module_from_file(join(CW_SOFTWARE_ROOT, ctlfile))
       
   356                 cls.info('loaded cubicweb-ctl plugin %s', ctlfile)
       
   357         templdir = cls.cubes_dir()
       
   358         for cube in cls.available_cubes():
       
   359             pluginfile = join(templdir, cube, 'ecplugin.py')
       
   360             initfile = join(templdir, cube, '__init__.py')
       
   361             if exists(pluginfile):
       
   362                 try:
       
   363                     __import__('cubes.%s.ecplugin' % cube)
       
   364                     cls.info('loaded cubicweb-ctl plugin from %s', cube)
       
   365                 except:
       
   366                     cls.exception('while loading plugin %s', pluginfile)
       
   367             elif exists(initfile):
       
   368                 try:
       
   369                     __import__('cubes.%s' % cube)
       
   370                 except:
       
   371                     cls.exception('while loading cube %s', cube)
       
   372             else:
       
   373                 cls.warning('no __init__ file in cube %s', cube) 
       
   374 
       
   375     @classmethod
       
   376     def init_available_cubes(cls):
       
   377         """cubes may register some sources (svnfile for instance) in their
       
   378         __init__ file, so they should be loaded early in the startup process
       
   379         """
       
   380         for cube in cls.available_cubes():
       
   381             try:
       
   382                 __import__('cubes.%s' % cube)
       
   383             except Exception, ex:
       
   384                 cls.warning("can't init cube %s: %s", cube, ex)
       
   385         
       
   386     cubicweb_vobject_path = set(['entities'])
       
   387     cube_vobject_path = set(['entities'])
       
   388 
       
   389     @classmethod
       
   390     def build_vregistry_path(cls, templpath, evobjpath=None, tvobjpath=None):
       
   391         """given a list of directories, return a list of sub files and
       
   392         directories that should be loaded by the application objects registry.
       
   393 
       
   394         :param evobjpath:
       
   395           optional list of sub-directories (or files without the .py ext) of
       
   396           the cubicweb library that should be tested and added to the output list
       
   397           if they exists. If not give, default to `cubicweb_vobject_path` class
       
   398           attribute.
       
   399         :param tvobjpath:
       
   400           optional list of sub-directories (or files without the .py ext) of
       
   401           directories given in `templpath` that should be tested and added to
       
   402           the output list if they exists. If not give, default to
       
   403           `cube_vobject_path` class attribute.
       
   404         """
       
   405         vregpath = cls.build_vregistry_cubicweb_path(evobjpath)
       
   406         vregpath += cls.build_vregistry_cube_path(templpath, tvobjpath)
       
   407         return vregpath
       
   408 
       
   409     @classmethod
       
   410     def build_vregistry_cubicweb_path(cls, evobjpath=None):
       
   411         vregpath = []
       
   412         if evobjpath is None:
       
   413             evobjpath = cls.cubicweb_vobject_path
       
   414         for subdir in evobjpath:
       
   415             path = join(CW_SOFTWARE_ROOT, subdir)
       
   416             if exists(path):
       
   417                 vregpath.append(path)
       
   418         return vregpath
       
   419 
       
   420     @classmethod
       
   421     def build_vregistry_cube_path(cls, templpath, tvobjpath=None):
       
   422         vregpath = []
       
   423         if tvobjpath is None:
       
   424             tvobjpath = cls.cube_vobject_path
       
   425         for directory in templpath:
       
   426             for subdir in tvobjpath:
       
   427                 path = join(directory, subdir)
       
   428                 if exists(path):
       
   429                     vregpath.append(path)
       
   430                 elif exists(path + '.py'):
       
   431                     vregpath.append(path + '.py')
       
   432         return vregpath
       
   433         
       
   434     def __init__(self):
       
   435         ConfigurationMixIn.__init__(self)
       
   436         self.adjust_sys_path()
       
   437         self.load_defaults()
       
   438         self.translations = {} 
       
   439 
       
   440     def adjust_sys_path(self):
       
   441         self.cls_adjust_sys_path()
       
   442         
       
   443     def init_log(self, logthreshold=None, debug=False, 
       
   444                  logfile=None, syslog=False):
       
   445         """init the log service"""
       
   446         if os.environ.get('APYCOT_ROOT'):
       
   447             logthreshold = logging.CRITICAL
       
   448             # redirect logs to stdout to avoid apycot output parsing failure
       
   449             handler = logging.StreamHandler(sys.stdout)
       
   450         else:
       
   451             if debug:
       
   452                 if logthreshold is None:
       
   453                     logthreshold = logging.DEBUG # LLDEBUG
       
   454                 handler = logging.StreamHandler()
       
   455             elif logfile is None:
       
   456                 if syslog:
       
   457                     from logging import handlers
       
   458                     handler = handlers.SysLogHandler()
       
   459                 else:
       
   460                     handler = logging.StreamHandler()
       
   461             else:
       
   462                 try:
       
   463                     handler = logging.FileHandler(logfile)
       
   464                 except IOError:
       
   465                     handler = logging.StreamHandler()
       
   466             if logthreshold is None:
       
   467                 thresholdname = self['log-threshold']
       
   468                 logthreshold = getattr(logging, THRESHOLD_MAP.get(thresholdname,
       
   469                                                                   thresholdname))
       
   470         # configure the root logger
       
   471         logger = logging.getLogger()
       
   472         logger.setLevel(logthreshold)
       
   473         # only addHandler and removeHandler method while I would like a
       
   474         # setHandler method, so do it this way :$
       
   475         logger.handlers = [handler]
       
   476         isatty = hasattr(sys.__stdout__, 'isatty') and sys.__stdout__.isatty()
       
   477         if debug and isatty:
       
   478             from logilab.common.logging_ext import ColorFormatter
       
   479             fmt = ColorFormatter(self.log_format, '%Y-%m-%d %H:%M:%S')
       
   480             def col_fact(record):
       
   481                 if 'XXX' in record.message:
       
   482                     return 'cyan'
       
   483                 if 'kick' in record.message:
       
   484                     return 'red'
       
   485             fmt.colorfilters.append(col_fact)
       
   486         else:
       
   487             fmt = logging.Formatter(self.log_format, '%Y-%m-%d %H:%M:%S')
       
   488         logger.handlers[0].setFormatter(fmt)
       
   489         # configure simpleTal logger
       
   490         logging.getLogger('simpleTAL').setLevel(logging.ERROR)
       
   491 
       
   492     def vregistry_path(self):
       
   493         """return a list of files or directories where the registry will look
       
   494         for application objects. By default return nothing in NoApp config.
       
   495         """
       
   496         return []
       
   497     
       
   498     def eproperty_definitions(self):
       
   499         cfg = self.persistent_options_configuration()
       
   500         for section, options in cfg.options_by_section():
       
   501             section = section.lower()
       
   502             for optname, optdict, value in options:
       
   503                 key = '%s.%s' % (section, optname)
       
   504                 type, vocab = self.map_option(optdict)
       
   505                 default = cfg.option_default(optname, optdict)
       
   506                 pdef = {'type': type, 'vocabulary': vocab, 'default': default,
       
   507                         'help': optdict['help'],
       
   508                         'sitewide': optdict.get('sitewide', False)}
       
   509                 yield key, pdef
       
   510                 
       
   511     def map_option(self, optdict):
       
   512         try:
       
   513             vocab = optdict['choices']
       
   514         except KeyError:
       
   515             vocab = optdict.get('vocabulary')
       
   516             if isinstance(vocab, Method):
       
   517                 vocab = getattr(self, vocab.method, ())
       
   518         return CFGTYPE2ETYPE_MAP[optdict['type']], vocab
       
   519 
       
   520     
       
   521 class CubicWebConfiguration(CubicWebNoAppConfiguration):
       
   522     """base class for cubicweb server and web configurations"""
       
   523     
       
   524     if CubicWebNoAppConfiguration.mode == 'test':
       
   525         root = os.environ['APYCOT_ROOT']
       
   526         REGISTRY_DIR = '%s/etc/cubicweb.d/' % root
       
   527         INSTANCE_DATA_DIR = REGISTRY_DIR
       
   528         RUNTIME_DIR = '/tmp/'
       
   529         MIGRATION_DIR = '%s/local/share/cubicweb/migration/' % root
       
   530         if not exists(REGISTRY_DIR):
       
   531             os.makedirs(REGISTRY_DIR)
       
   532     elif CubicWebNoAppConfiguration.mode == 'dev':
       
   533         REGISTRY_DIR = expanduser('~/etc/cubicweb.d/')
       
   534         INSTANCE_DATA_DIR = REGISTRY_DIR
       
   535         RUNTIME_DIR = '/tmp/'
       
   536         MIGRATION_DIR = join(CW_SOFTWARE_ROOT, 'misc', 'migration')
       
   537     else: #mode = 'installed'
       
   538         REGISTRY_DIR = '/etc/cubicweb.d/'
       
   539         INSTANCE_DATA_DIR = '/var/lib/cubicweb/instances/'
       
   540         RUNTIME_DIR = '/var/run/cubicweb/'
       
   541         MIGRATION_DIR = '/usr/share/cubicweb/migration/'
       
   542 
       
   543     # for some commands (creation...) we don't want to initialize gettext
       
   544     set_language = True
       
   545     # set this to true to avoid false error message while creating an application
       
   546     creating = False
       
   547     
       
   548     options = CubicWebNoAppConfiguration.options + (
       
   549         ('log-file',
       
   550          {'type' : 'string',
       
   551           'default': Method('default_log_file'),
       
   552           'help': 'file where output logs should be written',
       
   553           'group': 'main', 'inputlevel': 2,
       
   554           }),
       
   555         # email configuration
       
   556         ('smtp-host',
       
   557          {'type' : 'string',
       
   558           'default': 'mail',
       
   559           'help': 'hostname of the SMTP mail server',
       
   560           'group': 'email', 'inputlevel': 1,
       
   561           }),
       
   562         ('smtp-port',
       
   563          {'type' : 'int',
       
   564           'default': 25,
       
   565           'help': 'listening port of the SMTP mail server',
       
   566           'group': 'email', 'inputlevel': 1,
       
   567           }),
       
   568         ('sender-name',
       
   569          {'type' : 'string',
       
   570           'default': Method('default_application_id'), 
       
   571           'help': 'name used as HELO name for outgoing emails from the \
       
   572 repository.',
       
   573           'group': 'email', 'inputlevel': 2,
       
   574           }),
       
   575         ('sender-addr',
       
   576          {'type' : 'string',
       
   577           'default': 'devel@logilab.fr',
       
   578           'help': 'email address used as HELO address for outgoing emails from \
       
   579 the repository',
       
   580           'group': 'email', 'inputlevel': 1,
       
   581           }),
       
   582         )
       
   583 
       
   584     @classmethod
       
   585     def runtime_dir(cls):
       
   586         """run time directory for pid file..."""
       
   587         return env_path('CW_RUNTIME', cls.RUNTIME_DIR, 'run time')
       
   588     
       
   589     @classmethod
       
   590     def registry_dir(cls):
       
   591         """return the control directory"""
       
   592         return env_path('CW_REGISTRY', cls.REGISTRY_DIR, 'registry')
       
   593 
       
   594     @classmethod
       
   595     def instance_data_dir(cls):
       
   596         """return the instance data directory"""
       
   597         return env_path('CW_INSTANCE_DATA', cls.INSTANCE_DATA_DIR,
       
   598                         'additional data')
       
   599         
       
   600     @classmethod
       
   601     def migration_scripts_dir(cls):
       
   602         """cubicweb migration scripts directory"""
       
   603         return env_path('CW_MIGRATION', cls.MIGRATION_DIR, 'migration')
       
   604 
       
   605     @classmethod
       
   606     def config_for(cls, appid, config=None):
       
   607         """return a configuration instance for the given application identifier
       
   608         """
       
   609         config = config or guess_configuration(cls.application_home(appid))
       
   610         configcls = configuration_cls(config)
       
   611         return configcls(appid)
       
   612     
       
   613     @classmethod
       
   614     def possible_configurations(cls, appid):
       
   615         """return the name of possible configurations for the given
       
   616         application id
       
   617         """
       
   618         home = cls.application_home(appid)
       
   619         return possible_configurations(home)
       
   620     
       
   621     @classmethod
       
   622     def application_home(cls, appid):
       
   623         """return the home directory of the application with the given
       
   624         application id
       
   625         """
       
   626         home = join(cls.registry_dir(), appid)
       
   627         if not exists(home):
       
   628             raise ConfigurationError('no such application %s (check it exists with "cubicweb-ctl list")' % appid)
       
   629         return home
       
   630 
       
   631     MODES = ('common', 'repository', 'Any', 'web')
       
   632     MCOMPAT = {'all-in-one': MODES,
       
   633                'repository': ('common', 'repository', 'Any'),
       
   634                'twisted'   : ('common', 'web'),}
       
   635     @classmethod
       
   636     def accept_mode(cls, mode):
       
   637         #assert mode in cls.MODES, mode
       
   638         return mode in cls.MCOMPAT[cls.name]
       
   639             
       
   640     # default configuration methods ###########################################
       
   641     
       
   642     def default_application_id(self):
       
   643         """return the application identifier, useful for option which need this
       
   644         as default value
       
   645         """
       
   646         return self.appid
       
   647 
       
   648     def default_log_file(self):
       
   649         """return default path to the log file of the application'server"""
       
   650         if self.mode == 'dev':
       
   651             basepath = '/tmp/%s-%s' % (basename(self.appid), self.name)
       
   652             path = basepath + '.log'
       
   653             i = 1
       
   654             while exists(path) and i < 100: # arbitrary limit to avoid infinite loop
       
   655                 try:
       
   656                     file(path, 'a')
       
   657                     break
       
   658                 except IOError:
       
   659                     path = '%s-%s.log' % (basepath, i)
       
   660                     i += 1
       
   661             return path
       
   662         return '/var/log/cubicweb/%s-%s.log' % (self.appid, self.name)
       
   663     
       
   664     def default_pid_file(self):
       
   665         """return default path to the pid file of the application'server"""
       
   666         return join(self.runtime_dir(), '%s-%s.pid' % (self.appid, self.name))
       
   667     
       
   668     # instance methods used to get application specific resources #############
       
   669     
       
   670     def __init__(self, appid):
       
   671         self.appid = appid
       
   672         CubicWebNoAppConfiguration.__init__(self)
       
   673         self._cubes = None
       
   674         self._site_loaded = set()
       
   675         self.load_file_configuration(self.main_config_file())
       
   676 
       
   677     def adjust_sys_path(self):
       
   678         CubicWebNoAppConfiguration.adjust_sys_path(self)
       
   679         # adding apphome to python path is not usually necessary in production
       
   680         # environments, but necessary for tests
       
   681         if self.apphome and not self.apphome in sys.path:
       
   682             sys.path.insert(0, self.apphome)
       
   683 
       
   684     @property
       
   685     def apphome(self):
       
   686         return join(self.registry_dir(), self.appid)
       
   687     
       
   688     @property
       
   689     def appdatahome(self):
       
   690         return join(self.instance_data_dir(), self.appid)
       
   691         
       
   692     def init_cubes(self, cubes):
       
   693         assert self._cubes is None
       
   694         self._cubes = self.reorder_cubes(cubes)
       
   695         # load cubes'__init__.py file first
       
   696         for cube in cubes:
       
   697             __import__('cubes.%s' % cube)
       
   698         self.load_site_cubicweb()
       
   699         # reload config file in cases options are defined in cubes __init__
       
   700         # or site_cubicweb files
       
   701         self.load_file_configuration(self.main_config_file())
       
   702         # configuration initialization hook
       
   703         self.load_configuration()
       
   704         
       
   705     def cubes(self):
       
   706         """return the list of cubes used by this instance
       
   707 
       
   708         result is ordered from the top level cubes to inner dependencies
       
   709         cubes
       
   710         """
       
   711         assert self._cubes is not None
       
   712         return self._cubes
       
   713         
       
   714     def cubes_path(self):
       
   715         """return the list of path to cubes used by this instance, from outer
       
   716         most to inner most cubes
       
   717         """
       
   718         return [self.cube_dir(p) for p in self.cubes()]
       
   719 
       
   720     def add_cubes(self, cubes):
       
   721         """add given cubes to the list of used cubes"""
       
   722         if not isinstance(cubes, list):
       
   723             cubes = list(cubes)
       
   724         self._cubes = self.reorder_cubes(list(self._cubes) + cubes)
       
   725         
       
   726     def main_config_file(self):
       
   727         """return application's control configuration file"""
       
   728         return join(self.apphome, '%s.conf' % self.name)
       
   729             
       
   730     def save(self):
       
   731         """write down current configuration"""
       
   732         self.generate_config(open(self.main_config_file(), 'w'))
       
   733 
       
   734     @cached
       
   735     def instance_md5_version(self):
       
   736         import md5
       
   737         infos = []
       
   738         for pkg in self.cubes():
       
   739             version = self.cube_version(pkg)
       
   740             infos.append('%s-%s' % (pkg, version))
       
   741         return md5.new(';'.join(infos)).hexdigest()
       
   742                 
       
   743     def load_site_cubicweb(self):
       
   744         """load (web?) application's specific site_cubicweb file"""
       
   745         for path in reversed([self.apphome] + self.cubes_path()):
       
   746             sitefile = join(path, 'site_cubicweb.py')
       
   747             if exists(sitefile) and not sitefile in self._site_loaded:
       
   748                 self._load_site_cubicweb(sitefile)
       
   749                 self._site_loaded.add(sitefile)
       
   750             else:
       
   751                 sitefile = join(path, 'site_erudi.py')
       
   752                 if exists(sitefile) and not sitefile in self._site_loaded:
       
   753                     self._load_site_cubicweb(sitefile)
       
   754                     self._site_loaded.add(sitefile)
       
   755                     self.warning('site_erudi.py is deprecated, should be renamed to site_cubicweb.py')
       
   756                 
       
   757     def _load_site_cubicweb(self, sitefile):
       
   758         context = {}
       
   759         execfile(sitefile, context, context)
       
   760         self.info('%s loaded', sitefile)
       
   761         # cube specific options
       
   762         if context.get('options'):
       
   763             self.register_options(context['options'])
       
   764             self.load_defaults()
       
   765                 
       
   766     def load_configuration(self):
       
   767         """load application's configuration files"""
       
   768         super(CubicWebConfiguration, self).load_configuration()
       
   769         if self.apphome and self.set_language:
       
   770             # init gettext
       
   771             self._set_language()
       
   772             
       
   773     def init_log(self, logthreshold=None, debug=False, force=False):
       
   774         """init the log service"""
       
   775         if not force and hasattr(self, '_logging_initialized'):
       
   776             return
       
   777         self._logging_initialized = True
       
   778         CubicWebNoAppConfiguration.init_log(self, logthreshold, debug,
       
   779                                          logfile=self.get('log-file'))
       
   780         # read a config file if it exists
       
   781         logconfig = join(self.apphome, 'logging.conf')
       
   782         if exists(logconfig):
       
   783             logging.fileConfig(logconfig)
       
   784 
       
   785     def available_languages(self, *args):
       
   786         """return available translation for an application, by looking for
       
   787         compiled catalog
       
   788 
       
   789         take *args to be usable as a vocabulary method
       
   790         """
       
   791         from glob import glob
       
   792         yield 'en' # ensure 'en' is yielded even if no .mo found
       
   793         for path in glob(join(self.apphome, 'i18n',
       
   794                               '*', 'LC_MESSAGES', 'cubicweb.mo')):
       
   795             lang = path.split(os.sep)[-3]
       
   796             if lang != 'en':
       
   797                 yield lang
       
   798         
       
   799     def _set_language(self):
       
   800         """set language for gettext"""
       
   801         from gettext import translation
       
   802         path = join(self.apphome, 'i18n')
       
   803         for language in self.available_languages():
       
   804             self.info("loading language %s", language)
       
   805             try:
       
   806                 tr = translation('cubicweb', path, languages=[language])
       
   807                 self.translations[language] = tr.ugettext
       
   808             except (ImportError, AttributeError, IOError):
       
   809                 self.exception('localisation support error for language %s',
       
   810                                language)            
       
   811     
       
   812     def vregistry_path(self):
       
   813         """return a list of files or directories where the registry will look
       
   814         for application objects
       
   815         """
       
   816         templpath = list(reversed(self.cubes_path()))
       
   817         if self.apphome: # may be unset in tests
       
   818             templpath.append(self.apphome)
       
   819         return self.build_vregistry_path(templpath)
       
   820 
       
   821     def set_sources_mode(self, sources):
       
   822         if not 'all' in sources:
       
   823             print 'warning: ignoring specified sources, requires a repository '\
       
   824                   'configuration'
       
   825         
       
   826     def migration_handler(self):
       
   827         """return a migration handler instance"""
       
   828         from cubicweb.common.migration import MigrationHelper
       
   829         return MigrationHelper(self, verbosity=self.verbosity)
       
   830 
       
   831     def i18ncompile(self, langs=None):
       
   832         from cubicweb.common import i18n
       
   833         if langs is None:
       
   834             langs = self.available_languages()
       
   835         i18ndir = join(self.apphome, 'i18n')
       
   836         if not exists(i18ndir):
       
   837             create_dir(i18ndir)
       
   838         sourcedirs = [join(path, 'i18n') for path in self.cubes_path()]
       
   839         sourcedirs.append(self.i18n_lib_dir())
       
   840         return i18n.compile_i18n_catalogs(sourcedirs, i18ndir, langs)
       
   841 
       
   842         
       
   843 # alias to get a configuration instance from an application id
       
   844 application_configuration = CubicWebConfiguration.config_for        
       
   845 
       
   846 # map logilab.common.logger thresholds to logging thresholds
       
   847 THRESHOLD_MAP = {'LOG_DEBUG':  'DEBUG',
       
   848                  'LOG_INFO':   'INFO',
       
   849                  'LOG_NOTICE': 'INFO',
       
   850                  'LOG_WARN':   'WARNING',
       
   851                  'LOG_ERR':    'ERROR',
       
   852                  'LOG_CRIT':   'CRITICAL',
       
   853                  }
       
   854 
       
   855 from cubicweb import set_log_methods
       
   856 set_log_methods(CubicWebConfiguration, logging.getLogger('cubicweb.configuration'))