devctl.py 32.1 KB
Newer Older
1
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
3
4
5
6
7
8
9
10
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
11
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
12
13
14
15
16
17
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
18
19
"""additional cubicweb-ctl commands and command handlers for cubicweb and
cubicweb's cubes development
Adrien Di Mascio's avatar
Adrien Di Mascio committed
20
"""
21

Adrien Di Mascio's avatar
Adrien Di Mascio committed
22
23
__docformat__ = "restructuredtext en"

24
25
26
# *ctl module should limit the number of import to be imported as quickly as
# possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
# completion). So import locally in command helpers.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
27
import sys
28
from datetime import datetime
29
from os import mkdir, chdir, path as osp
30
from warnings import warn
Adrien Di Mascio's avatar
Adrien Di Mascio committed
31
32

from logilab.common import STD_BLACKLIST
33

Adrien Di Mascio's avatar
Adrien Di Mascio committed
34
from cubicweb.__pkginfo__ import version as cubicwebversion
35
from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, ExecutionError
36
from cubicweb.cwctl import CWCTL
37
from cubicweb.cwconfig import CubicWebNoAppConfiguration
38
39
from cubicweb.toolsutils import (SKEL_EXCLUDE, Command, copy_skeleton,
                                 underline_title)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
40
41
42
from cubicweb.web.webconfig import WebConfiguration
from cubicweb.server.serverconfig import ServerConfiguration

43

44
45
46
47
class DevConfiguration(ServerConfiguration, WebConfiguration):
    """dummy config to get full library schema and appobjects for
    a cube or for cubicweb (without a home)
    """
Adrien Di Mascio's avatar
Adrien Di Mascio committed
48
    creating = True
49
50
51
52
53
54
    cleanup_interface_sobjects = False

    cubicweb_appobject_path = (ServerConfiguration.cubicweb_appobject_path
                               | WebConfiguration.cubicweb_appobject_path)
    cube_appobject_path = (ServerConfiguration.cube_appobject_path
                           | WebConfiguration.cube_appobject_path)
Sylvain Thenault's avatar
Sylvain Thenault committed
55

56
    def __init__(self, *cubes):
57
58
59
60
        super(DevConfiguration, self).__init__(cubes and cubes[0] or None)
        if cubes:
            self._cubes = self.reorder_cubes(
                self.expand_cubes(cubes, with_recommends=True))
61
            self.load_site_cubicweb()
62
63
        else:
            self._cubes = ()
64

Adrien Di Mascio's avatar
Adrien Di Mascio committed
65
66
    @property
    def apphome(self):
Sylvain Thenault's avatar
Sylvain Thenault committed
67
        return None
68
69
70
71

    def available_languages(self):
        return self.cw_languages()

Sylvain Thenault's avatar
Sylvain Thenault committed
72
73
    def main_config_file(self):
        return None
74
    def init_log(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
75
76
77
        pass
    def load_configuration(self):
        pass
Sylvain Thenault's avatar
Sylvain Thenault committed
78
79
80
81
82
83
84
85
86
87
88
89
90
    def default_log_file(self):
        return None


def cleanup_sys_modules(config):
    # cleanup sys.modules, required when we're updating multiple cubes
    for name, mod in sys.modules.items():
        if mod is None:
            # duh ? logilab.common.os for instance
            del sys.modules[name]
            continue
        if not hasattr(mod, '__file__'):
            continue
91
92
93
        if mod.__file__ is None:
            # odd/rare but real
            continue
Sylvain Thenault's avatar
Sylvain Thenault committed
94
95
96
97
        for path in config.vregistry_path():
            if mod.__file__.startswith(path):
                del sys.modules[name]
                break
Sylvain Thénault's avatar
Sylvain Thénault committed
98
99
100
101
102
    # fresh rtags
    from cubicweb import rtags
    from cubicweb.web import uicfg
    rtags.RTAGS[:] = []
    reload(uicfg)
103

Adrien Di Mascio's avatar
Adrien Di Mascio committed
104
105
106
107
108
109
def generate_schema_pot(w, cubedir=None):
    """generate a pot file with schema specific i18n messages

    notice that relation definitions description and static vocabulary
    should be marked using '_' and extracted using xgettext
    """
110
    from cubicweb.cwvreg import CWRegistryStore
Adrien Di Mascio's avatar
Adrien Di Mascio committed
111
    if cubedir:
112
        cube = osp.split(cubedir)[-1]
113
114
115
116
        config = DevConfiguration(cube)
        depcubes = list(config._cubes)
        depcubes.remove(cube)
        libconfig = DevConfiguration(*depcubes)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
117
    else:
118
119
120
        config = DevConfiguration()
        cube = libconfig = None
    cleanup_sys_modules(config)
121
    schema = config.load_schema(remove_unused_rtypes=False)
122
    vreg = CWRegistryStore(config)
Sylvain Thenault's avatar
Sylvain Thenault committed
123
    # set_schema triggers objects registrations
Adrien Di Mascio's avatar
Adrien Di Mascio committed
124
125
    vreg.set_schema(schema)
    w(DEFAULT_POT_HEAD)
126
    _generate_schema_pot(w, vreg, schema, libconfig=libconfig)
127
128


129
130
def _generate_schema_pot(w, vreg, schema, libconfig=None):
    from copy import deepcopy
131
    from cubicweb.i18n import add_msg
132
    from cubicweb.web import uicfg
133
    from cubicweb.schema import NO_I18NCONTEXT, CONSTRAINTS
134
135
    w('# schema pot file, generated on %s\n'
      % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
136
137
138
    w('# \n')
    w('# singular and plural forms for each entity type\n')
    w('\n')
139
    vregdone = set()
140
    if libconfig is not None:
141
        from cubicweb.cwvreg import CWRegistryStore, clear_rtag_objects
142
        libschema = libconfig.load_schema(remove_unused_rtypes=False)
143
        afs = deepcopy(uicfg.autoform_section)
144
145
146
        appearsin_addmenu = deepcopy(uicfg.actionbox_appearsin_addmenu)
        clear_rtag_objects()
        cleanup_sys_modules(libconfig)
147
        libvreg = CWRegistryStore(libconfig)
148
        libvreg.set_schema(libschema) # trigger objects registration
149
        libafs = uicfg.autoform_section
150
151
152
        libappearsin_addmenu = uicfg.actionbox_appearsin_addmenu
        # prefill vregdone set
        list(_iter_vreg_objids(libvreg, vregdone))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
153
    else:
154
        libschema = {}
155
        afs = uicfg.autoform_section
156
        appearsin_addmenu = uicfg.actionbox_appearsin_addmenu
157
158
        for cstrtype in CONSTRAINTS:
            add_msg(w, cstrtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
159
    done = set()
160
161
162
    for eschema in sorted(schema.entities()):
        if eschema.type in libschema:
            done.add(eschema.description)
163
    for eschema in sorted(schema.entities()):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
164
        etype = eschema.type
165
166
167
        if etype not in libschema:
            add_msg(w, etype)
            add_msg(w, '%s_plural' % etype)
168
            if not eschema.final:
169
170
171
172
173
                add_msg(w, 'This %s' % etype)
                add_msg(w, 'New %s' % etype)
            if eschema.description and not eschema.description in done:
                done.add(eschema.description)
                add_msg(w, eschema.description)
174
        if eschema.final:
175
176
            continue
        for rschema, targetschemas, role in eschema.relation_definitions(True):
177
178
            if rschema.final:
                continue
179
            for tschema in targetschemas:
180
                fsections = afs.etype_get(eschema, rschema, role, tschema)
181
                if 'main_inlined' in fsections and \
182
                       (libconfig is None or not
183
                        'main_inlined' in libafs.etype_get(
184
                            eschema, rschema, role, tschema)):
185
186
                    add_msg(w, 'add a %s' % tschema,
                            'inlined:%s.%s.%s' % (etype, rschema, role))
187
                    add_msg(w, str(tschema),
188
                            'inlined:%s.%s.%s' % (etype, rschema, role))
189
                if appearsin_addmenu.etype_get(eschema, rschema, role, tschema):
190
191
                    if libconfig is not None and libappearsin_addmenu.etype_get(
                        eschema, rschema, role, tschema):
192
193
                        if eschema in libschema and tschema in libschema:
                            continue
194
195
196
197
198
199
200
201
202
203
204
205
                    if role == 'subject':
                        label = 'add %s %s %s %s' % (eschema, rschema,
                                                     tschema, role)
                        label2 = "creating %s (%s %%(linkto)s %s %s)" % (
                            tschema, eschema, rschema, tschema)
                    else:
                        label = 'add %s %s %s %s' % (tschema, rschema,
                                                     eschema, role)
                        label2 = "creating %s (%s %s %s %%(linkto)s)" % (
                            tschema, tschema, rschema, eschema)
                    add_msg(w, label)
                    add_msg(w, label2)
Sylvain Thénault's avatar
Sylvain Thénault committed
206
207
            # XXX also generate "creating ...' messages for actions in the
            # addrelated submenu
Adrien Di Mascio's avatar
Adrien Di Mascio committed
208
    w('# subject and object forms for each relation type\n')
209
    w('# (no object form for final or symmetric relation types)\n')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
210
    w('\n')
211
212
213
214
    for rschema in sorted(schema.relations()):
        if rschema.type in libschema:
            done.add(rschema.type)
            done.add(rschema.description)
215
    for rschema in sorted(schema.relations()):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
216
        rtype = rschema.type
217
        if rtype not in libschema:
218
219
            # bw compat, necessary until all translation of relation are done
            # properly...
220
            add_msg(w, rtype)
221
            done.add(rtype)
222
223
            if rschema.description and rschema.description not in done:
                add_msg(w, rschema.description)
224
            done.add(rschema.description)
225
226
227
            librschema = None
        else:
            librschema = libschema.rschema(rtype)
228
        # add context information only for non-metadata rtypes
229
        if rschema not in NO_I18NCONTEXT:
230
            libsubjects = librschema and librschema.subjects() or ()
231
            for subjschema in rschema.subjects():
232
233
                if not subjschema in libsubjects:
                    add_msg(w, rtype, subjschema.type)
234
        if not (rschema.final or rschema.symmetric):
235
            if rschema not in NO_I18NCONTEXT:
236
237
238
239
240
                libobjects = librschema and librschema.objects() or ()
                for objschema in rschema.objects():
                    if not objschema in libobjects:
                        add_msg(w, '%s_object' % rtype, objschema.type)
            if rtype not in libschema:
241
242
                # bw compat, necessary until all translation of relation are
                # done properly...
243
                add_msg(w, '%s_object' % rtype)
244
245
246
247
248
249
250
251
        for rdef in rschema.rdefs.itervalues():
            if not rdef.description or rdef.description in done:
                continue
            if (librschema is None or
                (rdef.subject, rdef.object) not in librschema.rdefs or
                librschema.rdefs[(rdef.subject, rdef.object)].description != rdef.description):
                add_msg(w, rdef.description)
            done.add(rdef.description)
252
    for objid in _iter_vreg_objids(vreg, vregdone):
253
254
255
        add_msg(w, '%s_description' % objid)
        add_msg(w, objid)

Sylvain Thénault's avatar
Sylvain Thénault committed
256

257
def _iter_vreg_objids(vreg, done):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
258
    for reg, objdict in vreg.items():
259
260
        if reg in ('boxes', 'contentnavigation'):
            continue
Adrien Di Mascio's avatar
Adrien Di Mascio committed
261
262
        for objects in objdict.values():
            for obj in objects:
Sylvain Thénault's avatar
Sylvain Thénault committed
263
                objid = '%s_%s' % (reg, obj.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
264
                if objid in done:
265
                    break
266
                pdefs = getattr(obj, 'cw_property_defs', {})
Sylvain Thénault's avatar
Sylvain Thénault committed
267
                if pdefs:
268
                    yield objid
Adrien Di Mascio's avatar
Adrien Di Mascio committed
269
                    done.add(objid)
270
                    break
271

272

Adrien Di Mascio's avatar
Adrien Di Mascio committed
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
DEFAULT_POT_HEAD = r'''msgid ""
msgstr ""
"Project-Id-Version: cubicweb %s\n"
"PO-Revision-Date: 2008-03-28 18:14+0100\n"
"Last-Translator: Logilab Team <contact@logilab.fr>\n"
"Language-Team: fr <contact@logilab.fr>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: cubicweb-devtools\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

''' % cubicwebversion


class UpdateCubicWebCatalogCommand(Command):
    """Update i18n catalogs for cubicweb library.
290

Adrien Di Mascio's avatar
Adrien Di Mascio committed
291
292
293
    It will regenerate cubicweb/i18n/xx.po files. You'll have then to edit those
    files to add translations of newly added messages.
    """
294
    name = 'i18ncubicweb'
295
    min_args = max_args = 0
Adrien Di Mascio's avatar
Adrien Di Mascio committed
296
297
298
299

    def run(self, args):
        """run the command with its specific arguments"""
        import shutil
300
        import tempfile
Adrien Di Mascio's avatar
Adrien Di Mascio committed
301
302
        import yams
        from logilab.common.fileutils import ensure_fs_mode
303
        from logilab.common.shellutils import globfind, find, rm
304
        from logilab.common.modutils import get_module_files
305
        from cubicweb.i18n import extract_from_tal, execute
306
        tempdir = tempfile.mkdtemp(prefix='cw-')
307
        cwi18ndir = WebConfiguration.i18n_lib_dir()
308
        print '-> extract schema messages.'
309
310
        schemapot = osp.join(tempdir, 'schema.pot')
        potfiles = [schemapot]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
311
312
313
314
315
316
        potfiles.append(schemapot)
        # explicit close necessary else the file may not be yet flushed when
        # we'll using it below
        schemapotstream = file(schemapot, 'w')
        generate_schema_pot(schemapotstream.write, cubedir=None)
        schemapotstream.close()
317
        print '-> extract TAL messages.'
318
319
320
        tali18nfile = osp.join(tempdir, 'tali18n.py')
        extract_from_tal(find(osp.join(BASEDIR, 'web'), ('.py', '.pt')),
                         tali18nfile)
321
        print '-> generate .pot files.'
322
323
324
325
326
327
        pyfiles = get_module_files(BASEDIR)
        pyfiles += globfind(osp.join(BASEDIR, 'misc', 'migration'), '*.py')
        schemafiles = globfind(osp.join(BASEDIR, 'schemas'), '*.py')
        jsfiles = globfind(osp.join(BASEDIR, 'web'), 'cub*.js')
        for id, files, lang in [('pycubicweb', pyfiles, None),
                                ('schemadescr', schemafiles, None),
Adrien Di Mascio's avatar
Adrien Di Mascio committed
328
329
                                ('yams', get_module_files(yams.__path__[0]), None),
                                ('tal', [tali18nfile], None),
330
                                ('js', jsfiles, 'java'),
Adrien Di Mascio's avatar
Adrien Di Mascio committed
331
                                ]:
332
            cmd = 'xgettext --no-location --omit-header -k_ -o %s %s'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
333
334
            if lang is not None:
                cmd += ' -L %s' % lang
335
            potfile = osp.join(tempdir, '%s.pot' % id)
336
            execute(cmd % (potfile, ' '.join('"%s"' % f for f in files)))
337
            if osp.exists(potfile):
338
339
                potfiles.append(potfile)
            else:
340
341
                print '-> WARNING: %s file was not generated' % potfile
        print '-> merging %i .pot files' % len(potfiles)
342
343
344
        cubicwebpot = osp.join(tempdir, 'cubicweb.pot')
        execute('msgcat -o %s %s'
                % (cubicwebpot, ' '.join('"%s"' % f for f in potfiles)))
345
        print '-> merging main pot file with existing translations.'
346
        chdir(cwi18ndir)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
347
        toedit = []
348
        for lang in CubicWebNoAppConfiguration.cw_languages():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
349
            target = '%s.po' % lang
350
351
            execute('msgmerge -N --sort-output -o "%snew" "%s" "%s"'
                    % (target, target, cubicwebpot))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
352
353
            ensure_fs_mode(target)
            shutil.move('%snew' % target, target)
354
            toedit.append(osp.abspath(target))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
355
        # cleanup
356
        rm(tempdir)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
357
        # instructions pour la suite
358
359
        print '-> regenerated CubicWeb\'s .po catalogs.'
        print '\nYou can now edit the following files:'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
360
        print '* ' + '\n* '.join(toedit)
361
        print 'when you are done, run "cubicweb-ctl i18ncube yourcube".'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
362
363


364
class UpdateCubeCatalogCommand(Command):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
365
366
367
    """Update i18n catalogs for cubes. If no cube is specified, update
    catalogs of all registered cubes.
    """
368
    name = 'i18ncube'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
369
    arguments = '[<cube>...]'
370

Adrien Di Mascio's avatar
Adrien Di Mascio committed
371
372
373
    def run(self, args):
        """run the command with its specific arguments"""
        if args:
374
            cubes = [DevConfiguration.cube_dir(cube) for cube in args]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
375
        else:
376
377
            cubes = [DevConfiguration.cube_dir(cube)
                     for cube in DevConfiguration.available_cubes()]
378
379
            cubes = [cubepath for cubepath in cubes
                     if osp.exists(osp.join(cubepath, 'i18n'))]
380
381
        if not update_cubes_catalogs(cubes):
            raise ExecutionError("update cubes i18n catalog failed")
Adrien Di Mascio's avatar
Adrien Di Mascio committed
382

383

Adrien Di Mascio's avatar
Adrien Di Mascio committed
384
385
def update_cubes_catalogs(cubes):
    for cubedir in cubes:
386
        if not osp.isdir(cubedir):
387
            print '-> ignoring %s that is not a directory.' % cubedir
Adrien Di Mascio's avatar
Adrien Di Mascio committed
388
            continue
389
        try:
390
            toedit = update_cube_catalogs(cubedir)
391
392
393
        except Exception:
            import traceback
            traceback.print_exc()
Sylvain Thénault's avatar
Sylvain Thénault committed
394
            print '-> error while updating catalogs for cube', cubedir
395
            return False
Sylvain Thénault's avatar
Sylvain Thénault committed
396
397
        else:
            # instructions pour la suite
398
399
400
401
402
403
            if toedit:
                print '-> regenerated .po catalogs for cube %s.' % cubedir
                print '\nYou can now edit the following files:'
                print '* ' + '\n* '.join(toedit)
                print ('When you are done, run "cubicweb-ctl i18ninstance '
                       '<yourinstance>" to see changes in your instances.')
404
            return True
Adrien Di Mascio's avatar
Adrien Di Mascio committed
405

406
407
def update_cube_catalogs(cubedir):
    import shutil
408
    import tempfile
409
410
    from logilab.common.fileutils import ensure_fs_mode
    from logilab.common.shellutils import find, rm
411
    from cubicweb.i18n import extract_from_tal, execute
412
    cube = osp.basename(osp.normpath(cubedir))
413
    tempdir = tempfile.mkdtemp()
414
    print underline_title('Updating i18n catalogs for cube %s' % cube)
415
    chdir(cubedir)
416
    if osp.exists(osp.join('i18n', 'entities.pot')):
417
        warn('entities.pot is deprecated, rename file to static-messages.pot (%s)'
418
419
420
421
             % osp.join('i18n', 'entities.pot'), DeprecationWarning)
        potfiles = [osp.join('i18n', 'entities.pot')]
    elif osp.exists(osp.join('i18n', 'static-messages.pot')):
        potfiles = [osp.join('i18n', 'static-messages.pot')]
422
423
    else:
        potfiles = []
424
    print '-> extract schema messages'
425
    schemapot = osp.join(tempdir, 'schema.pot')
426
427
428
429
430
431
    potfiles.append(schemapot)
    # explicit close necessary else the file may not be yet flushed when
    # we'll using it below
    schemapotstream = file(schemapot, 'w')
    generate_schema_pot(schemapotstream.write, cubedir)
    schemapotstream.close()
432
    print '-> extract TAL messages'
433
434
435
    tali18nfile = osp.join(tempdir, 'tali18n.py')
    ptfiles = find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST+('test',))
    extract_from_tal(ptfiles, tali18nfile)
436
    print '-> extract Javascript messages'
437
438
    jsfiles =  [jsfile for jsfile in find('.', '.js')
                if osp.basename(jsfile).startswith('cub')]
439
    if jsfiles:
440
441
442
        tmppotfile = osp.join(tempdir, 'js.pot')
        execute('xgettext --no-location --omit-header -k_ -L java '
                '--from-code=utf-8 -o %s %s' % (tmppotfile, ' '.join(jsfiles)))
443
        # no pot file created if there are no string to translate
444
        if osp.exists(tmppotfile):
445
            potfiles.append(tmppotfile)
446
    print '-> create cube-specific catalog'
447
    tmppotfile = osp.join(tempdir, 'generated.pot')
448
449
450
    cubefiles = find('.', '.py', blacklist=STD_BLACKLIST+('test',))
    cubefiles.append(tali18nfile)
    execute('xgettext --no-location --omit-header -k_ -o %s %s'
451
            % (tmppotfile, ' '.join('"%s"' % f for f in cubefiles)))
452
    if osp.exists(tmppotfile): # doesn't exists of no translation string found
453
        potfiles.append(tmppotfile)
454
    potfile = osp.join(tempdir, 'cube.pot')
455
    print '-> merging %i .pot files:' % len(potfiles)
456
457
    execute('msgcat -o %s %s' % (potfile,
                                 ' '.join('"%s"' % f for f in potfiles)))
458
    if not osp.exists(potfile):
459
460
461
462
        print 'no message catalog for cube', cube, 'nothing to translate'
        # cleanup
        rm(tempdir)
        return ()
463
    print '-> merging main pot file with existing translations:'
464
    chdir('i18n')
465
    toedit = []
466
    for lang in CubicWebNoAppConfiguration.cw_languages():
467
        print '-> language', lang
468
        cubepo = '%s.po' % lang
469
        if not osp.exists(cubepo):
470
471
            shutil.copy(potfile, cubepo)
        else:
472
            execute('msgmerge -N -s -o %snew %s %s' % (cubepo, cubepo, potfile))
473
474
            ensure_fs_mode(cubepo)
            shutil.move('%snew' % cubepo, cubepo)
475
        toedit.append(osp.abspath(cubepo))
476
477
478
479
480
    # cleanup
    rm(tempdir)
    return toedit


481
482
483
484
485
486
487
# XXX totally broken, fix it
# class LiveServerCommand(Command):
#     """Run a server from within a cube directory.
#     """
#     name = 'live-server'
#     arguments = ''
#     options = ()
488

489
490
491
492
#     def run(self, args):
#         """run the command with its specific arguments"""
#         from cubicweb.devtools.livetest import runserver
#         runserver()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
493
494


495
class NewCubeCommand(Command):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
496
497
498
    """Create a new cube.

    <cubename>
499
      the name of the new cube. It should be a valid python module name.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
500
501
502
    """
    name = 'newcube'
    arguments = '<cubename>'
503
    min_args = max_args = 1
504
    options = (
505
506
507
508
509
510
511
        ("layout",
         {'short': 'L', 'type' : 'choice', 'metavar': '<cube layout>',
          'default': 'simple', 'choices': ('simple', 'full'),
          'help': 'cube layout. You\'ll get a minimal cube with the "simple" \
layout, and a full featured cube with "full" layout.',
          }
         ),
512
513
514
515
516
        ("directory",
         {'short': 'd', 'type' : 'string', 'metavar': '<cubes directory>',
          'help': 'directory where the new cube should be created',
          }
         ),
517
518
519
520
521
522
        ("verbose",
         {'short': 'v', 'type' : 'yn', 'metavar': '<verbose>',
          'default': 'n',
          'help': 'verbose mode: will ask all possible configuration questions',
          }
         ),
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
        ("author",
         {'short': 'a', 'type' : 'string', 'metavar': '<author>',
          'default': 'LOGILAB S.A. (Paris, FRANCE)',
          'help': 'cube author',
          }
         ),
        ("author-email",
         {'short': 'e', 'type' : 'string', 'metavar': '<email>',
          'default': 'contact@logilab.fr',
          'help': 'cube author\'s email',
          }
         ),
        ("author-web-site",
         {'short': 'w', 'type' : 'string', 'metavar': '<web site>',
          'default': 'http://www.logilab.fr',
          'help': 'cube author\'s web site',
          }
         ),
541
542
543
544
545
546
        ("license",
         {'short': 'l', 'type' : 'choice', 'metavar': '<license>',
          'default': 'LGPL', 'choices': ('GPL', 'LGPL', ''),
          'help': 'cube license',
          }
         ),
547
548
        )

549
    LICENSES = {
550
551
552
553
554
555
556
557
558
559
560
561
562
563
        'LGPL': '''\
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
''',
564

565
566
567
568
569
570
571
572
573
574
575
576
577
578
        'GPL': '''\
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 2.1 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
''',
        '': '# INSERT LICENSE HERE'
579
        }
580

Adrien Di Mascio's avatar
Adrien Di Mascio committed
581
    def run(self, args):
582
        import re
583
        from logilab.common.shellutils import ASK
584
585
        cubename = args[0]
        if not re.match('[_A-Za-z][_A-Za-z0-9]*$', cubename):
586
587
            raise BadCommandUsage(
                'cube name must be a valid python module name')
588
        verbose = self.get('verbose')
589
        cubesdir = self.get('directory')
590
591
        if not cubesdir:
            cubespath = ServerConfiguration.cubes_search_path()
592
            if len(cubespath) > 1:
593
594
595
                raise BadCommandUsage(
                    "can't guess directory where to put the new cube."
                    " Please specify it using the --directory option")
596
            cubesdir = cubespath[0]
597
        if not osp.isdir(cubesdir):
598
            print "-> creating cubes directory", cubesdir
Adrien Di Mascio's avatar
Adrien Di Mascio committed
599
            try:
600
                mkdir(cubesdir)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
601
            except OSError, err:
602
603
604
605
606
607
                self.fail("failed to create directory %r\n(%s)"
                          % (cubesdir, err))
        cubedir = osp.join(cubesdir, cubename)
        if osp.exists(cubedir):
            self.fail("%s already exists !" % cubedir)
        skeldir = osp.join(BASEDIR, 'skeleton')
608
        default_name = 'cubicweb-%s' % cubename.lower().replace('_', '-')
609
        if verbose:
610
611
            distname = raw_input('Debian name for your cube ? [%s]): '
                                 % default_name).strip()
612
            if not distname:
613
                distname = default_name
614
            elif not distname.startswith('cubicweb-'):
615
                if ASK.confirm('Do you mean cubicweb-%s ?' % distname):
616
617
                    distname = 'cubicweb-' + distname
        else:
618
            distname = default_name
619
        if not re.match('[a-z][-a-z0-9]*$', distname):
620
621
622
623
            raise BadCommandUsage(
                'cube distname should be a valid debian package name')
        longdesc = shortdesc = raw_input(
            'Enter a short description for your cube: ')
624
        if verbose:
625
626
            longdesc = raw_input(
                'Enter a long description (leave empty to reuse the short one): ')
627
        dependencies = {'cubicweb': '>= %s' % cubicwebversion}
628
        if verbose:
629
            dependencies.update(self._ask_for_dependencies())
Adrien Di Mascio's avatar
Adrien Di Mascio committed
630
631
632
633
        context = {'cubename' : cubename,
                   'distname' : distname,
                   'shortdesc' : shortdesc,
                   'longdesc' : longdesc or shortdesc,
634
                   'dependencies' : dependencies,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
635
                   'version'  : cubicwebversion,
636
                   'year'  : str(datetime.now().year),
637
638
639
                   'author': self['author'],
                   'author-email': self['author-email'],
                   'author-web-site': self['author-web-site'],
640
641
                   'license': self['license'],
                   'long-license': self.LICENSES[self['license']],
Adrien Di Mascio's avatar
Adrien Di Mascio committed
642
                   }
643
644
645
        exclude = SKEL_EXCLUDE
        if self['layout'] == 'simple':
            exclude += ('sobjects.py*', 'precreate.py*', 'realdb_test*',
646
                        'cubes.*', 'uiprops.py*')
647
        copy_skeleton(skeldir, cubedir, context, exclude=exclude)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
648

649
    def _ask_for_dependencies(self):
650
651
        from logilab.common.shellutils import ASK
        from logilab.common.textutils import splitstrip
652
653
654
        depcubes = []
        for cube in ServerConfiguration.available_cubes():
            answer = ASK.ask("Depends on cube %s? " % cube,
655
656
                             ('N','y','skip','type'), 'N')
            if answer == 'y':
657
                depcubes.append(cube)
658
            if answer == 'type':
659
                depcubes = splitstrip(raw_input('type dependencies: '))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
660
                break
661
            elif answer == 'skip':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
662
                break
663
664
        return dict(('cubicweb-' + cube, ServerConfiguration.cube_version(cube))
                    for cube in depcubes)
665

666
667
668
669
670
671
672
673
674
675

class ExamineLogCommand(Command):
    """Examine a rql log file.

    will print out the following table

      total execution time || number of occurences || rql query

    sorted by descending total execution time

676
677
    chances are the lines at the top are the ones that will bring the higher
    benefit after optimisation. Start there.
678
    """
679
    arguments = 'rql.log'
680
    name = 'exlog'
681
    options = ()
682

683
684
685
    def run(self, args):
        import re
        requests = {}
686
        for filepath in args:
687
            try:
688
689
690
691
                stream = file(filepath)
            except OSError, ex:
                raise BadCommandUsage("can't open rql log file %s: %s"
                                      % (filepath, ex))
692
            for lineno, line in enumerate(stream):
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
                if not ' WHERE ' in line:
                    continue
                try:
                    rql, time = line.split('--')
                    rql = re.sub("(\'\w+': \d*)", '', rql)
                    if '{' in rql:
                        rql = rql[:rql.index('{')]
                    req = requests.setdefault(rql, [])
                    time.strip()
                    chunks = time.split()
                    clocktime = float(chunks[0][1:])
                    cputime = float(chunks[-3])
                    req.append( (clocktime, cputime) )
                except Exception, exc:
                    sys.stderr.write('Line %s: %s (%s)\n' % (lineno, exc, line))
708
        stat = []
709
        for rql, times in requests.iteritems():
710
711
712
            stat.append( (sum(time[0] for time in times),
                          sum(time[1] for time in times),
                          len(times), rql) )
713
714
        stat.sort()
        stat.reverse()
715
        total_time = sum(clocktime for clocktime, cputime, occ, rql in stat) * 0.01
716
717
        print 'Percentage;Cumulative Time (clock);Cumulative Time (CPU);Occurences;Query'
        for clocktime, cputime, occ, rql in stat:
718
719
            print '%.2f;%.2f;%.2f;%s;%s' % (clocktime/total_time, clocktime,
                                            cputime, occ, rql)
720

721

722
723
724
725
class GenerateSchema(Command):
    """Generate schema image for the given cube"""
    name = "schema"
    arguments = '<cube>'
726
    min_args = max_args = 1
727
728
    options = [
        ('output-file',
729
         {'type':'string', 'default': None,
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
          'metavar': '<file>', 'short':'o', 'help':'output image file',
          'input':False,
          }),
        ('viewer',
         {'type': 'string', 'default':None,
          'short': "d", 'metavar':'<cmd>',
          'help':'command use to view the generated file (empty for none)',
          }),
        ('show-meta',
         {'action': 'store_true', 'default':False,
          'short': "m", 'metavar': "<yN>",
          'help':'include meta and internal entities in schema',
          }),
        ('show-workflow',
         {'action': 'store_true', 'default':False,
          'short': "w", 'metavar': "<yN>",
          'help':'include workflow entities in schema',
          }),
        ('show-cw-user',
         {'action': 'store_true', 'default':False,
          'metavar': "<yN>",
          'help':'include cubicweb user entities in schema',
          }),
        ('exclude-type',
         {'type':'string', 'default':'',
          'short': "x", 'metavar': "<types>",
          'help':'coma separated list of entity types to remove from view',
          }),
        ('include-type',
         {'type':'string', 'default':'',
          'short': "i", 'metavar': "<types>",
          'help':'coma separated list of entity types to include in view',
          }),
        ]
764
765

    def run(self, args):
766
767
        from subprocess import Popen
        from tempfile import NamedTemporaryFile
768
        from logilab.common.textutils import splitstrip
769
770
771
        from yams import schema2dot, BASE_TYPES
        from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
                                     WORKFLOW_TYPES, INTERNAL_TYPES)
772
        cubes = splitstrip(args[0])
773
        dev_conf = DevConfiguration(*cubes)
774
775
776
777
778
        schema = dev_conf.load_schema()
        out, viewer = self['output-file'], self['viewer']
        if out is None:
            tmp_file = NamedTemporaryFile(suffix=".svg")
            out = tmp_file.name
779
780
781
782
783
784
785
786
787
788
        skiptypes = BASE_TYPES | SCHEMA_TYPES
        if not self['show-meta']:
            skiptypes |=  META_RTYPES | SYSTEM_RTYPES | INTERNAL_TYPES
        if not self['show-workflow']:
            skiptypes |= WORKFLOW_TYPES
        if not self['show-cw-user']:
            skiptypes |= set(('CWUser', 'CWGroup', 'EmailAddress'))
        skiptypes |= set(self['exclude-type'].split(','))
        skiptypes -= set(self['include-type'].split(','))
        schema2dot.schema2dot(schema, out, skiptypes=skiptypes)
789
790
791
792
        if viewer:
            p = Popen((viewer, out))
            p.wait()

793
794
795
796
797
798
799
800
801
802

class GenerateQUnitHTML(Command):
    """Generate a QUnit html file to see test in your browser"""
    name = "qunit-html"
    arguments = '<test file> [<dependancy js file>...]'

    def run(self, args):
        from cubicweb.devtools.qunit import make_qunit_html
        print make_qunit_html(args[0], args[1:])

803
for cmdcls in (UpdateCubicWebCatalogCommand,
804
               UpdateCubeCatalogCommand,
805
806
807
808
809
810
811
               #LiveServerCommand,
               NewCubeCommand,
               ExamineLogCommand,
               GenerateSchema,
               GenerateQUnitHTML,
               ):
    CWCTL.register(cmdcls)