devctl.py 31.9 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
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 CubicWebVRegistry
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 = CubicWebVRegistry(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 CubicWebVRegistry, clear_rtag_objects
142
        libschema = libconfig.load_schema(remove_unused_rtypes=False)
143
        afs = deepcopy(uicfg.autoform_section)
144
145
146
147
148
        appearsin_addmenu = deepcopy(uicfg.actionbox_appearsin_addmenu)
        clear_rtag_objects()
        cleanup_sys_modules(libconfig)
        libvreg = CubicWebVRegistry(libconfig)
        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()
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'))]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
380
381
        update_cubes_catalogs(cubes)

382

Adrien Di Mascio's avatar
Adrien Di Mascio committed
383
384
def update_cubes_catalogs(cubes):
    for cubedir in cubes:
385
        if not osp.isdir(cubedir):
386
            print '-> ignoring %s that is not a directory.' % cubedir
Adrien Di Mascio's avatar
Adrien Di Mascio committed
387
            continue
388
        try:
389
            toedit = update_cube_catalogs(cubedir)
390
391
392
        except Exception:
            import traceback
            traceback.print_exc()
Sylvain Thénault's avatar
Sylvain Thénault committed
393
394
395
            print '-> error while updating catalogs for cube', cubedir
        else:
            # instructions pour la suite
396
397
398
399
400
401
            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.')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
402

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


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

486
487
488
489
#     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
490
491


492
class NewCubeCommand(Command):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
493
494
495
    """Create a new cube.

    <cubename>
496
      the name of the new cube. It should be a valid python module name.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
497
498
499
    """
    name = 'newcube'
    arguments = '<cubename>'
500
    min_args = max_args = 1
501
    options = (
502
503
504
505
506
507
508
        ("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.',
          }
         ),
509
510
511
512
513
        ("directory",
         {'short': 'd', 'type' : 'string', 'metavar': '<cubes directory>',
          'help': 'directory where the new cube should be created',
          }
         ),
514
515
516
517
518
519
        ("verbose",
         {'short': 'v', 'type' : 'yn', 'metavar': '<verbose>',
          'default': 'n',
          'help': 'verbose mode: will ask all possible configuration questions',
          }
         ),
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
        ("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',
          }
         ),
538
539
540
541
542
543
        ("license",
         {'short': 'l', 'type' : 'choice', 'metavar': '<license>',
          'default': 'LGPL', 'choices': ('GPL', 'LGPL', ''),
          'help': 'cube license',
          }
         ),
544
545
        )

546
    LICENSES = {
547
548
549
550
551
552
553
554
555
556
557
558
559
560
        '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/>.
''',
561

562
563
564
565
566
567
568
569
570
571
572
573
574
575
        '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'
576
        }
577

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

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

663
664
665
666
667
668
669
670
671
672

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

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

680
681
682
    def run(self, args):
        import re
        requests = {}
683
        for filepath in args:
684
            try:
685
686
687
688
                stream = file(filepath)
            except OSError, ex:
                raise BadCommandUsage("can't open rql log file %s: %s"
                                      % (filepath, ex))
689
            for lineno, line in enumerate(stream):
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
                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))
705
        stat = []
706
        for rql, times in requests.iteritems():
707
708
709
            stat.append( (sum(time[0] for time in times),
                          sum(time[1] for time in times),
                          len(times), rql) )
710
711
        stat.sort()
        stat.reverse()
712
        total_time = sum(clocktime for clocktime, cputime, occ, rql in stat) * 0.01
713
714
        print 'Percentage;Cumulative Time (clock);Cumulative Time (CPU);Occurences;Query'
        for clocktime, cputime, occ, rql in stat:
715
716
            print '%.2f;%.2f;%.2f;%s;%s' % (clocktime/total_time, clocktime,
                                            cputime, occ, rql)
717

718

719
720
721
722
class GenerateSchema(Command):
    """Generate schema image for the given cube"""
    name = "schema"
    arguments = '<cube>'
723
    min_args = max_args = 1
724
725
726
727
728
729
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
    options = [
        ('output-file',
         {'type':'file', 'default': None,
          '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',
          }),
        ]
761
762

    def run(self, args):
763
764
        from subprocess import Popen
        from tempfile import NamedTemporaryFile
765
        from logilab.common.textutils import splitstrip
766
767
768
        from yams import schema2dot, BASE_TYPES
        from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
                                     WORKFLOW_TYPES, INTERNAL_TYPES)
769
        cubes = splitstrip(args[0])
770
        dev_conf = DevConfiguration(*cubes)
771
772
773
774
775
        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
776
777
778
779
780
781
782
783
784
785
        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)
786
787
788
789
        if viewer:
            p = Popen((viewer, out))
            p.wait()

790
791
792
793
794
795
796
797
798
799

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:])

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