devctl.py 26.7 KB
Newer Older
Adrien Di Mascio's avatar
Adrien Di Mascio committed
1
2
3
4
"""additional cubicweb-ctl commands and command handlers for cubicweb and cubicweb's
cubes development

:organization: Logilab
5
:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
6
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
7
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
Adrien Di Mascio's avatar
Adrien Di Mascio committed
8
9
10
"""
__docformat__ = "restructuredtext en"

11
12
13
# *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
14
import sys
15
from datetime import datetime
16
from os import mkdir, chdir
Adrien Di Mascio's avatar
Adrien Di Mascio committed
17
from os.path import join, exists, abspath, basename, normpath, split, isdir
18
from warnings import warn
Adrien Di Mascio's avatar
Adrien Di Mascio committed
19
20

from logilab.common import STD_BLACKLIST
21
22
from logilab.common.clcommands import register_commands, pop_arg

Adrien Di Mascio's avatar
Adrien Di Mascio committed
23
from cubicweb.__pkginfo__ import version as cubicwebversion
24
from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage
25
from cubicweb.toolsutils import Command, copy_skeleton, underline_title
Adrien Di Mascio's avatar
Adrien Di Mascio committed
26
27
28
from cubicweb.web.webconfig import WebConfiguration
from cubicweb.server.serverconfig import ServerConfiguration

29

30
31
32
33
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
34
    creating = True
35
36
37
38
39
40
    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
41

42
    def __init__(self, *cubes):
43
44
45
46
47
48
        super(DevConfiguration, self).__init__(cubes and cubes[0] or None)
        if cubes:
            self._cubes = self.reorder_cubes(
                self.expand_cubes(cubes, with_recommends=True))
        else:
            self._cubes = ()
49

Adrien Di Mascio's avatar
Adrien Di Mascio committed
50
51
    @property
    def apphome(self):
Sylvain Thenault's avatar
Sylvain Thenault committed
52
53
54
        return None
    def main_config_file(self):
        return None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
55
56
57
58
    def init_log(self, debug=None):
        pass
    def load_configuration(self):
        pass
Sylvain Thenault's avatar
Sylvain Thenault committed
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
    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
        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
76
77
78
79
80
    # fresh rtags
    from cubicweb import rtags
    from cubicweb.web import uicfg
    rtags.RTAGS[:] = []
    reload(uicfg)
81

Adrien Di Mascio's avatar
Adrien Di Mascio committed
82
83
84
85
86
87
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
    """
88
    from cubicweb.cwvreg import CubicWebVRegistry
Adrien Di Mascio's avatar
Adrien Di Mascio committed
89
    if cubedir:
90
91
92
93
94
        cube = split(cubedir)[-1]
        config = DevConfiguration(cube)
        depcubes = list(config._cubes)
        depcubes.remove(cube)
        libconfig = DevConfiguration(*depcubes)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
95
    else:
96
97
98
        config = DevConfiguration()
        cube = libconfig = None
    cleanup_sys_modules(config)
99
    schema = config.load_schema(remove_unused_rtypes=False)
100
    vreg = CubicWebVRegistry(config)
Sylvain Thenault's avatar
Sylvain Thenault committed
101
    # set_schema triggers objects registrations
Adrien Di Mascio's avatar
Adrien Di Mascio committed
102
103
    vreg.set_schema(schema)
    w(DEFAULT_POT_HEAD)
104
    _generate_schema_pot(w, vreg, schema, libconfig=libconfig)
105
106


107
108
def _generate_schema_pot(w, vreg, schema, libconfig=None):
    from copy import deepcopy
109
    from cubicweb.i18n import add_msg
110
    from cubicweb.web import uicfg
111
    from cubicweb.schema import META_RTYPES, SYSTEM_RTYPES, CONSTRAINTS
112
    no_context_rtypes = META_RTYPES | SYSTEM_RTYPES
113
    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
114
115
116
    w('# \n')
    w('# singular and plural forms for each entity type\n')
    w('\n')
117
    vregdone = set()
118
    if libconfig is not None:
119
        from cubicweb.cwvreg import CubicWebVRegistry, clear_rtag_objects
120
        libschema = libconfig.load_schema(remove_unused_rtypes=False)
121
        afs = deepcopy(uicfg.autoform_section)
122
123
124
125
126
        appearsin_addmenu = deepcopy(uicfg.actionbox_appearsin_addmenu)
        clear_rtag_objects()
        cleanup_sys_modules(libconfig)
        libvreg = CubicWebVRegistry(libconfig)
        libvreg.set_schema(libschema) # trigger objects registration
127
        libafs = uicfg.autoform_section
128
129
130
        libappearsin_addmenu = uicfg.actionbox_appearsin_addmenu
        # prefill vregdone set
        list(_iter_vreg_objids(libvreg, vregdone))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
131
    else:
132
        libschema = {}
133
        afs = uicfg.autoform_section
134
        appearsin_addmenu = uicfg.actionbox_appearsin_addmenu
135
136
        for cstrtype in CONSTRAINTS:
            add_msg(w, cstrtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
137
    done = set()
138
    for eschema in sorted(schema.entities()):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
139
        etype = eschema.type
140
141
142
        if etype not in libschema:
            add_msg(w, etype)
            add_msg(w, '%s_plural' % etype)
143
            if not eschema.final:
144
145
146
147
148
                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)
149
        if eschema.final:
150
151
            continue
        for rschema, targetschemas, role in eschema.relation_definitions(True):
152
153
            if rschema.final:
                continue
154
            for tschema in targetschemas:
155
                fsections = afs.etype_get(eschema, rschema, role, tschema)
156
                if 'main_inlined' in fsections and \
157
                       (libconfig is None or not
158
                        'main_inlined' in libafs.etype_get(
159
                            eschema, rschema, role, tschema)):
160
161
                    add_msg(w, 'add a %s' % tschema,
                            'inlined:%s.%s.%s' % (etype, rschema, role))
162
                    add_msg(w, str(tschema),
163
                            'inlined:%s.%s.%s' % (etype, rschema, role))
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
                if appearsin_addmenu.etype_get(eschema, rschema, role, tschema) and \
                       (libconfig is None or not
                        libappearsin_addmenu.etype_get(eschema, rschema, role, tschema)):
                    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
179
180
            # XXX also generate "creating ...' messages for actions in the
            # addrelated submenu
Adrien Di Mascio's avatar
Adrien Di Mascio committed
181
    w('# subject and object forms for each relation type\n')
182
    w('# (no object form for final or symmetric relation types)\n')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
183
    w('\n')
184
    for rschema in sorted(schema.relations()):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
185
        rtype = rschema.type
186
187
188
189
190
191
192
193
194
195
        if rtype not in libschema:
            # bw compat, necessary until all translation of relation are done properly...
            add_msg(w, rtype)
            if rschema.description and rschema.description not in done:
                done.add(rschema.description)
                add_msg(w, rschema.description)
            done.add(rtype)
            librschema = None
        else:
            librschema = libschema.rschema(rtype)
196
        # add context information only for non-metadata rtypes
197
        if rschema not in no_context_rtypes:
198
            libsubjects = librschema and librschema.subjects() or ()
199
            for subjschema in rschema.subjects():
200
201
                if not subjschema in libsubjects:
                    add_msg(w, rtype, subjschema.type)
202
        if not (schema.rschema(rtype).final or rschema.symmetric):
203
            if rschema not in no_context_rtypes:
204
205
206
207
208
209
210
211
                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:
                # bw compat, necessary until all translation of relation are done properly...
                add_msg(w, '%s_object' % rtype)
    for objid in _iter_vreg_objids(vreg, vregdone):
212
213
214
        add_msg(w, '%s_description' % objid)
        add_msg(w, objid)

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

216
def _iter_vreg_objids(vreg, done):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
217
218
219
    for reg, objdict in vreg.items():
        for objects in objdict.values():
            for obj in objects:
Sylvain Thénault's avatar
Sylvain Thénault committed
220
                objid = '%s_%s' % (reg, obj.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
221
                if objid in done:
222
                    break
Sylvain Thénault's avatar
Sylvain Thénault committed
223
224
225
226
227
                try: # XXX < 3.6 bw compat
                    pdefs = obj.property_defs
                except AttributeError:
                    pdefs = getattr(obj, 'cw_property_defs', {})
                if pdefs:
228
                    yield objid
Adrien Di Mascio's avatar
Adrien Di Mascio committed
229
                    done.add(objid)
230
                    break
231

232

233
LANGS = ('en', 'fr', 'es')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
I18NDIR = join(BASEDIR, 'i18n')
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.
252

Adrien Di Mascio's avatar
Adrien Di Mascio committed
253
254
255
    It will regenerate cubicweb/i18n/xx.po files. You'll have then to edit those
    files to add translations of newly added messages.
    """
256
    name = 'i18ncubicweb'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
257
258
259
260
261
262

    def run(self, args):
        """run the command with its specific arguments"""
        if args:
            raise BadCommandUsage('Too much arguments')
        import shutil
263
        import tempfile
Adrien Di Mascio's avatar
Adrien Di Mascio committed
264
265
        import yams
        from logilab.common.fileutils import ensure_fs_mode
266
        from logilab.common.shellutils import globfind, find, rm
267
        from logilab.common.modutils import get_module_files
268
        from cubicweb.i18n import extract_from_tal, execute
269
        tempdir = tempfile.mkdtemp()
270
        potfiles = [join(I18NDIR, 'static-messages.pot')]
271
        print '-> extract schema messages.'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
272
273
274
275
276
277
278
        schemapot = join(tempdir, 'schema.pot')
        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()
279
        print '-> extract TAL messages.'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
280
281
        tali18nfile = join(tempdir, 'tali18n.py')
        extract_from_tal(find(join(BASEDIR, 'web'), ('.py', '.pt')), tali18nfile)
282
        print '-> generate .pot files.'
283
        for id, files, lang in [('pycubicweb', get_module_files(BASEDIR) + list(globfind(join(BASEDIR, 'misc', 'migration'), '*.py')), None),
284
                                ('schemadescr', globfind(join(BASEDIR, 'schemas'), '*.py'), None),
Adrien Di Mascio's avatar
Adrien Di Mascio committed
285
286
                                ('yams', get_module_files(yams.__path__[0]), None),
                                ('tal', [tali18nfile], None),
287
                                ('js', globfind(join(BASEDIR, 'web'), 'cub*.js'), 'java'),
Adrien Di Mascio's avatar
Adrien Di Mascio committed
288
                                ]:
289
            cmd = 'xgettext --no-location --omit-header -k_ -o %s %s'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
290
291
            if lang is not None:
                cmd += ' -L %s' % lang
292
            potfile = join(tempdir, '%s.pot' % id)
293
            execute(cmd % (potfile, ' '.join('"%s"' % f for f in files)))
294
295
296
            if exists(potfile):
                potfiles.append(potfile)
            else:
297
298
                print '-> WARNING: %s file was not generated' % potfile
        print '-> merging %i .pot files' % len(potfiles)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
299
        cubicwebpot = join(tempdir, 'cubicweb.pot')
300
        execute('msgcat -o %s %s' % (cubicwebpot, ' '.join('"%s"' % f for f in potfiles)))
301
        print '-> merging main pot file with existing translations.'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
302
303
304
305
        chdir(I18NDIR)
        toedit = []
        for lang in LANGS:
            target = '%s.po' % lang
306
            execute('msgmerge -N --sort-output -o "%snew" "%s" "%s"' % (target, target, cubicwebpot))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
307
308
309
310
            ensure_fs_mode(target)
            shutil.move('%snew' % target, target)
            toedit.append(abspath(target))
        # cleanup
311
        rm(tempdir)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
312
        # instructions pour la suite
313
314
        print '-> regenerated CubicWeb\'s .po catalogs.'
        print '\nYou can now edit the following files:'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
315
        print '* ' + '\n* '.join(toedit)
316
        print 'when you are done, run "cubicweb-ctl i18ncube yourcube".'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
317
318
319
320
321
322


class UpdateTemplateCatalogCommand(Command):
    """Update i18n catalogs for cubes. If no cube is specified, update
    catalogs of all registered cubes.
    """
323
    name = 'i18ncube'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
324
    arguments = '[<cube>...]'
325

Adrien Di Mascio's avatar
Adrien Di Mascio committed
326
327
328
    def run(self, args):
        """run the command with its specific arguments"""
        if args:
329
            cubes = [DevConfiguration.cube_dir(cube) for cube in args]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
330
        else:
331
332
            cubes = [DevConfiguration.cube_dir(cube)
                     for cube in DevConfiguration.available_cubes()]
333
            cubes = [cubepath for cubepath in cubes if exists(join(cubepath, 'i18n'))]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
334
335
        update_cubes_catalogs(cubes)

336

Adrien Di Mascio's avatar
Adrien Di Mascio committed
337
338
def update_cubes_catalogs(cubes):
    for cubedir in cubes:
Sylvain Thénault's avatar
Sylvain Thénault committed
339
        toedit = []
Adrien Di Mascio's avatar
Adrien Di Mascio committed
340
        if not isdir(cubedir):
341
            print '-> ignoring %s that is not a directory.' % cubedir
Adrien Di Mascio's avatar
Adrien Di Mascio committed
342
            continue
343
344
345
346
347
        try:
            toedit += update_cube_catalogs(cubedir)
        except Exception:
            import traceback
            traceback.print_exc()
Sylvain Thénault's avatar
Sylvain Thénault committed
348
349
350
351
352
353
354
355
            print '-> error while updating catalogs for cube', cubedir
        else:
            # instructions pour la suite
            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
356

357
358
def update_cube_catalogs(cubedir):
    import shutil
359
    import tempfile
360
361
    from logilab.common.fileutils import ensure_fs_mode
    from logilab.common.shellutils import find, rm
362
    from cubicweb.i18n import extract_from_tal, execute
363
364
    toedit = []
    cube = basename(normpath(cubedir))
365
    tempdir = tempfile.mkdtemp()
366
    print underline_title('Updating i18n catalogs for cube %s' % cube)
367
    chdir(cubedir)
368
369
370
371
372
373
374
375
    if exists(join('i18n', 'entities.pot')):
        warn('entities.pot is deprecated, rename file to static-messages.pot (%s)'
             % join('i18n', 'entities.pot'), DeprecationWarning)
        potfiles = [join('i18n', 'entities.pot')]
    elif exists(join('i18n', 'static-messages.pot')):
        potfiles = [join('i18n', 'static-messages.pot')]
    else:
        potfiles = []
376
    print '-> extract schema messages'
377
378
379
380
381
382
383
    schemapot = join(tempdir, 'schema.pot')
    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()
384
    print '-> extract TAL messages'
385
386
    tali18nfile = join(tempdir, 'tali18n.py')
    extract_from_tal(find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST+('test',)), tali18nfile)
387
    print '-> extract Javascript messages'
388
389
390
391
392
393
394
395
    jsfiles =  [jsfile for jsfile in find('.', '.js') if basename(jsfile).startswith('cub')]
    if jsfiles:
        tmppotfile = join(tempdir, 'js.pot')
        execute('xgettext --no-location --omit-header -k_ -L java --from-code=utf-8 -o %s %s'
                % (tmppotfile, ' '.join(jsfiles)))
        # no pot file created if there are no string to translate
        if exists(tmppotfile):
            potfiles.append(tmppotfile)
396
    print '-> create cube-specific catalog'
397
398
399
400
    tmppotfile = join(tempdir, 'generated.pot')
    cubefiles = find('.', '.py', blacklist=STD_BLACKLIST+('test',))
    cubefiles.append(tali18nfile)
    execute('xgettext --no-location --omit-header -k_ -o %s %s'
401
            % (tmppotfile, ' '.join('"%s"' % f for f in cubefiles)))
402
403
404
    if exists(tmppotfile): # doesn't exists of no translation string found
        potfiles.append(tmppotfile)
    potfile = join(tempdir, 'cube.pot')
405
    print '-> merging %i .pot files:' % len(potfiles)
406
407
    execute('msgcat -o %s %s' % (potfile,
                                 ' '.join('"%s"' % f for f in potfiles)))
408
    print '-> merging main pot file with existing translations:'
409
410
    chdir('i18n')
    for lang in LANGS:
411
        print '-> language', lang
412
413
414
415
        cubepo = '%s.po' % lang
        if not exists(cubepo):
            shutil.copy(potfile, cubepo)
        else:
416
            execute('msgmerge -N -s -o %snew %s %s' % (cubepo, cubepo, potfile))
417
418
419
420
421
422
423
424
            ensure_fs_mode(cubepo)
            shutil.move('%snew' % cubepo, cubepo)
        toedit.append(abspath(cubepo))
    # cleanup
    rm(tempdir)
    return toedit


425
426
427
428
429
430
431
# XXX totally broken, fix it
# class LiveServerCommand(Command):
#     """Run a server from within a cube directory.
#     """
#     name = 'live-server'
#     arguments = ''
#     options = ()
432

433
434
435
436
#     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
437
438


439
class NewCubeCommand(Command):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
440
441
442
443
444
445
446
447
    """Create a new cube.

    <cubename>
      the name of the new cube
    """
    name = 'newcube'
    arguments = '<cubename>'

448
    options = (
449
450
451
452
453
        ("directory",
         {'short': 'd', 'type' : 'string', 'metavar': '<cubes directory>',
          'help': 'directory where the new cube should be created',
          }
         ),
454
455
456
457
458
459
        ("verbose",
         {'short': 'v', 'type' : 'yn', 'metavar': '<verbose>',
          'default': 'n',
          'help': 'verbose mode: will ask all possible configuration questions',
          }
         ),
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
        ("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',
          }
         ),
478
479
        )

480

Adrien Di Mascio's avatar
Adrien Di Mascio committed
481
    def run(self, args):
482
        from logilab.common.shellutils import ASK
Adrien Di Mascio's avatar
Adrien Di Mascio committed
483
484
485
        if len(args) != 1:
            raise BadCommandUsage("exactly one argument (cube name) is expected")
        cubename, = args
486
        verbose = self.get('verbose')
487
        cubesdir = self.get('directory')
488
489
        if not cubesdir:
            cubespath = ServerConfiguration.cubes_search_path()
490
491
492
493
494
            if len(cubespath) > 1:
                raise BadCommandUsage("can't guess directory where to put the new cube."
                                      " Please specify it using the --directory option")
            cubesdir = cubespath[0]
        if not isdir(cubesdir):
495
            print "-> creating cubes directory", cubesdir
Adrien Di Mascio's avatar
Adrien Di Mascio committed
496
            try:
497
                mkdir(cubesdir)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
498
            except OSError, err:
499
                self.fail("failed to create directory %r\n(%s)" % (cubesdir, err))
500
        cubedir = join(cubesdir, cubename)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
501
502
503
        if exists(cubedir):
            self.fail("%s already exists !" % (cubedir))
        skeldir = join(BASEDIR, 'skeleton')
504
        default_name = 'cubicweb-%s' % cubename.lower()
505
        if verbose:
506
            distname = raw_input('Debian name for your cube ? [%s]): ' % default_name).strip()
507
            if not distname:
508
                distname = default_name
509
            elif not distname.startswith('cubicweb-'):
510
                if ASK.confirm('Do you mean cubicweb-%s ?' % distname):
511
512
                    distname = 'cubicweb-' + distname
        else:
513
            distname = default_name
514

515
516
        longdesc = shortdesc = raw_input('Enter a short description for your cube: ')
        if verbose:
517
            longdesc = raw_input('Enter a long description (leave empty to reuse the short one): ')
518
        dependencies = {}
519
        if verbose:
520
            dependencies = self._ask_for_dependencies()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
521
522
523
524
        context = {'cubename' : cubename,
                   'distname' : distname,
                   'shortdesc' : shortdesc,
                   'longdesc' : longdesc or shortdesc,
525
                   'dependencies' : dict((dep, None) for dep in dependencies),
Adrien Di Mascio's avatar
Adrien Di Mascio committed
526
                   'version'  : cubicwebversion,
527
                   'year'  : str(datetime.now().year),
528
529
530
                   'author': self['author'],
                   'author-email': self['author-email'],
                   'author-web-site': self['author-web-site'],
Adrien Di Mascio's avatar
Adrien Di Mascio committed
531
532
533
                   }
        copy_skeleton(skeldir, cubedir, context)

534
    def _ask_for_dependencies(self):
535
536
        from logilab.common.shellutils import ASK
        from logilab.common.textutils import splitstrip
Adrien Di Mascio's avatar
Adrien Di Mascio committed
537
538
        includes = []
        for stdtype in ServerConfiguration.available_cubes():
539
540
541
            answer = ASK.ask("Depends on cube %s? " % stdtype,
                             ('N','y','skip','type'), 'N')
            if answer == 'y':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
542
                includes.append(stdtype)
543
            if answer == 'type':
544
                includes = splitstrip(raw_input('type dependencies: '))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
545
                break
546
            elif answer == 'skip':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
547
548
                break
        return includes
549

550
551
552
553
554
555
556
557
558
559

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

560
561
    chances are the lines at the top are the ones that will bring the higher
    benefit after optimisation. Start there.
562
    """
563
    arguments = '< rql.log'
564
565
566
    name = 'exlog'
    options = (
        )
567

568
569
570
571
572
    def run(self, args):
        if args:
            raise BadCommandUsage("no argument expected")
        import re
        requests = {}
573
        for lineno, line in enumerate(sys.stdin):
574
575
576
            if not ' WHERE ' in line:
                continue
            #sys.stderr.write( line )
577
578
579
            try:
                rql, time = line.split('--')
                rql = re.sub("(\'\w+': \d*)", '', rql)
580
581
                if '{' in rql:
                    rql = rql[:rql.index('{')]
582
583
584
                req = requests.setdefault(rql, [])
                time.strip()
                chunks = time.split()
585
                clocktime = float(chunks[0][1:])
586
                cputime = float(chunks[-3])
587
                req.append( (clocktime, cputime) )
588
            except Exception, exc:
589
                sys.stderr.write('Line %s: %s (%s)\n' % (lineno, exc, line))
590
591
592

        stat = []
        for rql, times in requests.items():
593
594
595
            stat.append( (sum(time[0] for time in times),
                          sum(time[1] for time in times),
                          len(times), rql) )
596
597
598

        stat.sort()
        stat.reverse()
599

600
601
602
603
        total_time = sum(clocktime for clocktime, cputime, occ, rql in stat)*0.01
        print 'Percentage;Cumulative Time (clock);Cumulative Time (CPU);Occurences;Query'
        for clocktime, cputime, occ, rql in stat:
            print '%.2f;%.2f;%.2f;%s;%s' % (clocktime/total_time, clocktime, cputime, occ, rql)
604

605

606
607
608
609
610
611
612
613
class GenerateSchema(Command):
    """Generate schema image for the given cube"""
    name = "schema"
    arguments = '<cube>'
    options = [('output-file', {'type':'file', 'default': None,
                 'metavar': '<file>', 'short':'o', 'help':'output image file',
                 'input':False}),
               ('viewer', {'type': 'string', 'default':None,
614
                'short': "d", 'metavar':'<cmd>',
615
616
                 'help':'command use to view the generated file (empty for none)'}
               ),
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
               ('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'}
               ),
637
638
639
              ]

    def run(self, args):
640
641
        from subprocess import Popen
        from tempfile import NamedTemporaryFile
642
        from logilab.common.textutils import splitstrip
643
644
645
        from yams import schema2dot, BASE_TYPES
        from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
                                     WORKFLOW_TYPES, INTERNAL_TYPES)
646
        cubes = splitstrip(pop_arg(args, 1))
647
        dev_conf = DevConfiguration(*cubes)
648
649
650
651
652
        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
653
654
655
656
657
658
659
660
661
662
        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)
663
664
665
666
        if viewer:
            p = Popen((viewer, out))
            p.wait()

Adrien Di Mascio's avatar
Adrien Di Mascio committed
667
668
register_commands((UpdateCubicWebCatalogCommand,
                   UpdateTemplateCatalogCommand,
669
                   #LiveServerCommand,
670
                   NewCubeCommand,
671
                   ExamineLogCommand,
672
                   GenerateSchema,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
673
                   ))