devtools/devctl.py
changeset 0 b97547f5f1fa
child 57 3ab952845448
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """additional cubicweb-ctl commands and command handlers for cubicweb and cubicweb's
       
     2 cubes development
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 import sys
       
    11 from os import walk, mkdir, chdir, listdir
       
    12 from os.path import join, exists, abspath, basename, normpath, split, isdir
       
    13 
       
    14 
       
    15 from logilab.common import STD_BLACKLIST
       
    16 from logilab.common.modutils import get_module_files
       
    17 from logilab.common.textutils import get_csv
       
    18 
       
    19 from cubicweb import CW_SOFTWARE_ROOT as BASEDIR
       
    20 from cubicweb.__pkginfo__ import version as cubicwebversion
       
    21 from cubicweb import BadCommandUsage
       
    22 from cubicweb.toolsutils import Command, register_commands, confirm, copy_skeleton
       
    23 from cubicweb.web.webconfig import WebConfiguration
       
    24 from cubicweb.server.serverconfig import ServerConfiguration
       
    25 
       
    26 
       
    27 class DevConfiguration(ServerConfiguration, WebConfiguration):
       
    28     """dummy config to get full library schema and entities"""
       
    29     creating = True
       
    30     def __init__(self, appid=None, cube=None):
       
    31         self._cube = cube
       
    32         super(DevConfiguration, self).__init__(appid)
       
    33         if self._cube is None:
       
    34             self._cubes = ()
       
    35         else:
       
    36             self._cubes = self.expand_cubes(self.cube_dependencies(self._cube))
       
    37         
       
    38 #    def adjust_sys_path(self):
       
    39 #        # update python path if necessary
       
    40 #        if not self.cubes_dir() in sys.path:
       
    41 #            sys.path.insert(0, self.cubes_dir())
       
    42     
       
    43     @property
       
    44     def apphome(self):
       
    45         return self.appid
       
    46     
       
    47     def init_log(self, debug=None):
       
    48         pass
       
    49     def load_configuration(self):
       
    50         pass
       
    51 
       
    52     cubicweb_vobject_path = ServerConfiguration.cubicweb_vobject_path | WebConfiguration.cubicweb_vobject_path
       
    53     cube_vobject_path = ServerConfiguration.cube_vobject_path | WebConfiguration.cube_vobject_path
       
    54 
       
    55 
       
    56 def generate_schema_pot(w, cubedir=None):
       
    57     """generate a pot file with schema specific i18n messages
       
    58 
       
    59     notice that relation definitions description and static vocabulary
       
    60     should be marked using '_' and extracted using xgettext
       
    61     """
       
    62     from cubicweb.cwvreg import CubicWebRegistry
       
    63     cube = cubedir and split(cubedir)[-1]
       
    64     config = DevConfiguration(join(BASEDIR, 'web'), cube)
       
    65     if cubedir:
       
    66         libschema = config.load_schema()
       
    67         config = DevConfiguration(cubedir, cube)
       
    68         schema = config.load_schema()
       
    69     else:
       
    70         schema = config.load_schema()
       
    71         libschema = None
       
    72         config.cleanup_interface_sobjects = False
       
    73     vreg = CubicWebRegistry(config)
       
    74     vreg.set_schema(schema)
       
    75     vreg.register_objects(config.vregistry_path())
       
    76     w(DEFAULT_POT_HEAD)
       
    77     _generate_schema_pot(w, vreg, schema, libschema=libschema,
       
    78                          cube=cube)
       
    79     # cleanup sys.modules, required when we're updating multiple cubes
       
    80     for name, mod in sys.modules.items():
       
    81         if mod is None:
       
    82             # duh ? logilab.common.os for instance
       
    83             del sys.modules[name]
       
    84             continue
       
    85         if not hasattr(mod, '__file__'):
       
    86             continue
       
    87         for path in config.vregistry_path():
       
    88             if mod.__file__.startswith(path):
       
    89                 del sys.modules[name]
       
    90                 break
       
    91                 
       
    92 def _generate_schema_pot(w, vreg, schema, libschema=None, cube=None):
       
    93     from mx.DateTime import now
       
    94     from cubicweb.common.i18n import add_msg
       
    95     w('# schema pot file, generated on %s\n' % now().strftime('%Y-%m-%d %H:%M:%S'))
       
    96     w('# \n')
       
    97     w('# singular and plural forms for each entity type\n')
       
    98     w('\n')
       
    99     if libschema is not None:
       
   100         entities = [e for e in schema.entities() if not e in libschema]
       
   101     else:
       
   102         entities = schema.entities()
       
   103     done = set()
       
   104     for eschema in sorted(entities):
       
   105         etype = eschema.type
       
   106         add_msg(w, etype)
       
   107         add_msg(w, '%s_plural' % etype)
       
   108         if not eschema.is_final():
       
   109             add_msg(w, 'This %s' % etype)
       
   110             add_msg(w, 'New %s' % etype)
       
   111             add_msg(w, 'add a %s' % etype)
       
   112             add_msg(w, 'remove this %s' % etype)
       
   113         if eschema.description and not eschema.description in done:
       
   114             done.add(eschema.description)
       
   115             add_msg(w, eschema.description)
       
   116     w('# subject and object forms for each relation type\n')
       
   117     w('# (no object form for final relation types)\n')
       
   118     w('\n')
       
   119     if libschema is not None:
       
   120         relations = [r for r in schema.relations() if not r in libschema]
       
   121     else:
       
   122         relations = schema.relations()
       
   123     for rschema in sorted(set(relations)):
       
   124         rtype = rschema.type
       
   125         add_msg(w, rtype)
       
   126         if not (schema.rschema(rtype).is_final() or rschema.symetric):
       
   127             add_msg(w, '%s_object' % rtype)
       
   128         if rschema.description and rschema.description not in done:
       
   129             done.add(rschema.description)
       
   130             add_msg(w, rschema.description)
       
   131     w('# add related box generated message\n')
       
   132     w('\n')
       
   133     for eschema in schema.entities():
       
   134         if eschema.is_final():
       
   135             continue
       
   136         entity = vreg.etype_class(eschema)(None, None)
       
   137         for x, rschemas in (('subject', eschema.subject_relations()),
       
   138                             ('object', eschema.object_relations())):
       
   139             for rschema in rschemas:
       
   140                 if rschema.is_final():
       
   141                     continue
       
   142                 for teschema in rschema.targets(eschema, x):
       
   143                     if defined_in_library(libschema, eschema, rschema, teschema, x):
       
   144                         continue
       
   145                     if entity.relation_mode(rschema.type, teschema.type, x) == 'create':
       
   146                         if x == 'subject':
       
   147                             label = 'add %s %s %s %s' % (eschema, rschema, teschema, x)
       
   148                             label2 = "creating %s (%s %%(linkto)s %s %s)" % (teschema, eschema, rschema, teschema)
       
   149                         else:
       
   150                             label = 'add %s %s %s %s' % (teschema, rschema, eschema, x)
       
   151                             label2 = "creating %s (%s %s %s %%(linkto)s)" % (teschema, teschema, rschema, eschema)
       
   152                         add_msg(w, label)
       
   153                         add_msg(w, label2)
       
   154     cube = (cube or 'cubicweb') + '.'
       
   155     done = set()
       
   156     for reg, objdict in vreg.items():
       
   157         for objects in objdict.values():
       
   158             for obj in objects:
       
   159                 objid = '%s_%s' % (reg, obj.id)
       
   160                 if objid in done:
       
   161                     continue
       
   162                 if obj.__module__.startswith(cube) and obj.property_defs:
       
   163                     add_msg(w, '%s_description' % objid)
       
   164                     add_msg(w, objid)
       
   165                     done.add(objid)
       
   166                     
       
   167 def defined_in_library(libschema, etype, rtype, tetype, x):
       
   168     """return true if the given relation definition exists in cubicweb's library"""
       
   169     if libschema is None:
       
   170         return False
       
   171     if x == 'subject':
       
   172         subjtype, objtype = etype, tetype
       
   173     else:
       
   174         subjtype, objtype = tetype, etype
       
   175     try:
       
   176         return libschema.rschema(rtype).has_rdef(subjtype, objtype)
       
   177     except KeyError:
       
   178         return False
       
   179 
       
   180 
       
   181 LANGS = ('en', 'fr')
       
   182 I18NDIR = join(BASEDIR, 'i18n')
       
   183 DEFAULT_POT_HEAD = r'''msgid ""
       
   184 msgstr ""
       
   185 "Project-Id-Version: cubicweb %s\n"
       
   186 "PO-Revision-Date: 2008-03-28 18:14+0100\n"
       
   187 "Last-Translator: Logilab Team <contact@logilab.fr>\n"
       
   188 "Language-Team: fr <contact@logilab.fr>\n"
       
   189 "MIME-Version: 1.0\n"
       
   190 "Content-Type: text/plain; charset=UTF-8\n"
       
   191 "Content-Transfer-Encoding: 8bit\n"
       
   192 "Generated-By: cubicweb-devtools\n"
       
   193 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
       
   194 
       
   195 ''' % cubicwebversion
       
   196 
       
   197 
       
   198 class UpdateCubicWebCatalogCommand(Command):
       
   199     """Update i18n catalogs for cubicweb library.
       
   200     
       
   201     It will regenerate cubicweb/i18n/xx.po files. You'll have then to edit those
       
   202     files to add translations of newly added messages.
       
   203     """
       
   204     name = 'i18nlibupdate'
       
   205 
       
   206     def run(self, args):
       
   207         """run the command with its specific arguments"""
       
   208         if args:
       
   209             raise BadCommandUsage('Too much arguments')
       
   210         import shutil
       
   211         from tempfile import mktemp
       
   212         import yams
       
   213         from logilab.common.fileutils import ensure_fs_mode
       
   214         from logilab.common.shellutils import find, rm
       
   215         from cubicweb.common.i18n import extract_from_tal, execute
       
   216         tempdir = mktemp()
       
   217         mkdir(tempdir)
       
   218         potfiles = [join(I18NDIR, 'entities.pot')]
       
   219         print '******** extract schema messages'
       
   220         schemapot = join(tempdir, 'schema.pot')
       
   221         potfiles.append(schemapot)
       
   222         # explicit close necessary else the file may not be yet flushed when
       
   223         # we'll using it below
       
   224         schemapotstream = file(schemapot, 'w')
       
   225         generate_schema_pot(schemapotstream.write, cubedir=None)
       
   226         schemapotstream.close()
       
   227         print '******** extract TAL messages'
       
   228         tali18nfile = join(tempdir, 'tali18n.py')
       
   229         extract_from_tal(find(join(BASEDIR, 'web'), ('.py', '.pt')), tali18nfile)
       
   230         print '******** .pot files generation'
       
   231         for id, files, lang in [('cubicweb', get_module_files(BASEDIR) + find(join(BASEDIR, 'misc', 'migration'), '.py'), None),
       
   232                                 ('schemadescr', find(join(BASEDIR, 'schemas'), '.py'), None),
       
   233                                 ('yams', get_module_files(yams.__path__[0]), None),
       
   234                                 ('tal', [tali18nfile], None),
       
   235                                 ('js', find(join(BASEDIR, 'web'), '.js'), 'java'),
       
   236                                 ]:
       
   237             cmd = 'xgettext --no-location --omit-header -k_ -o %s %s'
       
   238             if lang is not None:
       
   239                 cmd += ' -L %s' % lang
       
   240             potfiles.append(join(tempdir, '%s.pot' % id))
       
   241             execute(cmd % (potfiles[-1], ' '.join(files)))
       
   242         print '******** merging .pot files'
       
   243         cubicwebpot = join(tempdir, 'cubicweb.pot')
       
   244         execute('msgcat %s > %s' % (' '.join(potfiles), cubicwebpot))
       
   245         print '******** merging main pot file with existing translations'
       
   246         chdir(I18NDIR)
       
   247         toedit = []
       
   248         for lang in LANGS:
       
   249             target = '%s.po' % lang
       
   250             execute('msgmerge -N --sort-output  %s %s > %snew' % (target, cubicwebpot, target))
       
   251             ensure_fs_mode(target)
       
   252             shutil.move('%snew' % target, target)
       
   253             toedit.append(abspath(target))
       
   254         # cleanup
       
   255         rm(tempdir)
       
   256         # instructions pour la suite
       
   257         print '*' * 72
       
   258         print 'you can now edit the following files:'
       
   259         print '* ' + '\n* '.join(toedit)
       
   260         print
       
   261         print "then you'll have to update cubes catalogs using the i18nupdate command"
       
   262 
       
   263 
       
   264 class UpdateTemplateCatalogCommand(Command):
       
   265     """Update i18n catalogs for cubes. If no cube is specified, update
       
   266     catalogs of all registered cubes.
       
   267     """
       
   268     name = 'i18nupdate'
       
   269     arguments = '[<cube>...]'
       
   270     
       
   271     def run(self, args):
       
   272         """run the command with its specific arguments"""
       
   273         CUBEDIR = DevConfiguration.cubes_dir()
       
   274         if args:
       
   275             cubes = [join(CUBEDIR, app) for app in args]
       
   276         else:
       
   277             cubes = [join(CUBEDIR, app) for app in listdir(CUBEDIR)
       
   278                          if exists(join(CUBEDIR, app, 'i18n'))]
       
   279         update_cubes_catalogs(cubes)
       
   280 
       
   281 def update_cubes_catalogs(cubes):
       
   282     import shutil
       
   283     from tempfile import mktemp
       
   284     from logilab.common.fileutils import ensure_fs_mode
       
   285     from logilab.common.shellutils import find, rm
       
   286     from cubicweb.common.i18n import extract_from_tal, execute
       
   287     toedit = []
       
   288     for cubedir in cubes:
       
   289         cube = basename(normpath(cubedir))
       
   290         if not isdir(cubedir):
       
   291             print 'unknown cube', cube
       
   292             continue
       
   293         tempdir = mktemp()
       
   294         mkdir(tempdir)
       
   295         print '*' * 72
       
   296         print 'updating %s cube...' % cube
       
   297         chdir(cubedir)
       
   298         potfiles = [join('i18n', scfile) for scfile in ('entities.pot',)
       
   299                     if exists(join('i18n', scfile))]
       
   300         print '******** extract schema messages'
       
   301         schemapot = join(tempdir, 'schema.pot')
       
   302         potfiles.append(schemapot)
       
   303         # explicit close necessary else the file may not be yet flushed when
       
   304         # we'll using it below
       
   305         schemapotstream = file(schemapot, 'w')
       
   306         generate_schema_pot(schemapotstream.write, cubedir)
       
   307         schemapotstream.close()
       
   308         print '******** extract TAL messages'
       
   309         tali18nfile = join(tempdir, 'tali18n.py')
       
   310         extract_from_tal(find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST+('test',)), tali18nfile)
       
   311         print '******** extract Javascript messages'
       
   312         jsfiles =  find('.', '.js')
       
   313         if jsfiles:
       
   314             tmppotfile = join(tempdir, 'js.pot')
       
   315             execute('xgettext --no-location --omit-header -k_ -L java --from-code=utf-8 -o %s %s'
       
   316                     % (tmppotfile, ' '.join(jsfiles)))
       
   317             # no pot file created if there are no string to translate
       
   318             if exists(tmppotfile): 
       
   319                 potfiles.append(tmppotfile)
       
   320         print '******** create cube specific catalog'
       
   321         tmppotfile = join(tempdir, 'generated.pot')
       
   322         cubefiles = find('.', '.py', blacklist=STD_BLACKLIST+('test',))
       
   323         cubefiles.append(tali18nfile)
       
   324         execute('xgettext --no-location --omit-header -k_ -o %s %s'
       
   325                 % (tmppotfile, ' '.join(cubefiles)))
       
   326         if exists(tmppotfile): # doesn't exists of no translation string found
       
   327             potfiles.append(tmppotfile)
       
   328         potfile = join(tempdir, 'cube.pot')
       
   329         print '******** merging .pot files'
       
   330         execute('msgcat %s > %s' % (' '.join(potfiles), potfile))
       
   331         print '******** merging main pot file with existing translations'
       
   332         chdir('i18n')
       
   333         for lang in LANGS:
       
   334             print '****', lang
       
   335             cubepo = '%s.po' % lang
       
   336             if not exists(cubepo):
       
   337                 shutil.copy(potfile, cubepo)
       
   338             else:
       
   339                 execute('msgmerge -N -s %s %s > %snew' % (cubepo, potfile, cubepo))
       
   340                 ensure_fs_mode(cubepo)
       
   341                 shutil.move('%snew' % cubepo, cubepo)
       
   342             toedit.append(abspath(cubepo))
       
   343         # cleanup
       
   344         rm(tempdir)
       
   345     # instructions pour la suite
       
   346     print '*' * 72
       
   347     print 'you can now edit the following files:'
       
   348     print '* ' + '\n* '.join(toedit)
       
   349 
       
   350 
       
   351 class LiveServerCommand(Command):
       
   352     """Run a server from within a cube directory.
       
   353     """
       
   354     name = 'live-server'
       
   355     arguments = ''
       
   356     options = ()
       
   357     
       
   358     def run(self, args):
       
   359         """run the command with its specific arguments"""
       
   360         from cubicweb.devtools.livetest import runserver
       
   361         runserver()
       
   362 
       
   363 
       
   364 class NewTemplateCommand(Command):
       
   365     """Create a new cube.
       
   366 
       
   367     <cubename>
       
   368       the name of the new cube
       
   369     """
       
   370     name = 'newcube'
       
   371     arguments = '<cubename>'
       
   372 
       
   373     
       
   374     def run(self, args):
       
   375         if len(args) != 1:
       
   376             raise BadCommandUsage("exactly one argument (cube name) is expected")
       
   377         cubename, = args
       
   378         if ServerConfiguration.mode != "dev":
       
   379             self.fail("you can only create new cubes in development mode")
       
   380         cubedir = ServerConfiguration.CUBES_DIR
       
   381         if not isdir(cubedir):
       
   382             print "creating apps directory", cubedir
       
   383             try:
       
   384                 mkdir(cubedir)
       
   385             except OSError, err:
       
   386                 self.fail("failed to create directory %r\n(%s)" % (cubedir, err))
       
   387         cubedir = join(cubedir, cubename)
       
   388         if exists(cubedir):
       
   389             self.fail("%s already exists !" % (cubedir))
       
   390         skeldir = join(BASEDIR, 'skeleton')
       
   391         distname = raw_input('Debian name for your cube (just type enter to use the cube name): ').strip()
       
   392         if not distname:
       
   393             distname = 'cubicweb-%s' % cubename.lower()
       
   394         elif not distname.startswith('cubicweb-'):
       
   395             if confirm('do you mean cubicweb-%s ?' % distname):
       
   396                 distname = 'cubicweb-' + distname
       
   397         shortdesc = raw_input('Enter a short description for your cube: ')
       
   398         longdesc = raw_input('Enter a long description (or nothing if you want to reuse the short one): ')
       
   399         includes = self._ask_for_dependancies()
       
   400         if len(includes) == 1:
       
   401             dependancies = '%r,' % includes[0]
       
   402         else:
       
   403             dependancies = ', '.join(repr(cube) for cube in includes)
       
   404         from mx.DateTime import now
       
   405         context = {'cubename' : cubename,
       
   406                    'distname' : distname,
       
   407                    'shortdesc' : shortdesc,
       
   408                    'longdesc' : longdesc or shortdesc,
       
   409                    'dependancies' : dependancies,
       
   410                    'version'  : cubicwebversion,
       
   411                    'year'  : str(now().year),
       
   412                    }
       
   413         copy_skeleton(skeldir, cubedir, context)
       
   414 
       
   415     def _ask_for_dependancies(self):
       
   416         includes = []
       
   417         for stdtype in ServerConfiguration.available_cubes():
       
   418             ans = raw_input("Depends on cube %s? (N/y/s(kip)/t(ype)"
       
   419                             % stdtype).lower().strip()
       
   420             if ans == 'y':
       
   421                 includes.append(stdtype)
       
   422             if ans == 't':
       
   423                 includes = get_csv(raw_input('type dependancies: '))
       
   424                 break
       
   425             elif ans == 's':
       
   426                 break
       
   427         return includes
       
   428     
       
   429         
       
   430 register_commands((UpdateCubicWebCatalogCommand,
       
   431                    UpdateTemplateCatalogCommand,
       
   432                    LiveServerCommand,
       
   433                    NewTemplateCommand,
       
   434                    ))