devctl.py 31.3 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# 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
91
92
93
94
    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
95
96
97
98
99
    # fresh rtags
    from cubicweb import rtags
    from cubicweb.web import uicfg
    rtags.RTAGS[:] = []
    reload(uicfg)
100

Adrien Di Mascio's avatar
Adrien Di Mascio committed
101
102
103
104
105
106
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
    """
107
    from cubicweb.cwvreg import CubicWebVRegistry
Adrien Di Mascio's avatar
Adrien Di Mascio committed
108
    if cubedir:
109
        cube = osp.split(cubedir)[-1]
110
111
112
113
        config = DevConfiguration(cube)
        depcubes = list(config._cubes)
        depcubes.remove(cube)
        libconfig = DevConfiguration(*depcubes)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
114
    else:
115
116
117
        config = DevConfiguration()
        cube = libconfig = None
    cleanup_sys_modules(config)
118
    schema = config.load_schema(remove_unused_rtypes=False)
119
    vreg = CubicWebVRegistry(config)
Sylvain Thenault's avatar
Sylvain Thenault committed
120
    # set_schema triggers objects registrations
Adrien Di Mascio's avatar
Adrien Di Mascio committed
121
122
    vreg.set_schema(schema)
    w(DEFAULT_POT_HEAD)
123
    _generate_schema_pot(w, vreg, schema, libconfig=libconfig)
124
125


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

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

239
def _iter_vreg_objids(vreg, done):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
240
    for reg, objdict in vreg.items():
241
242
        if reg in ('boxes', 'contentnavigation'):
            continue
Adrien Di Mascio's avatar
Adrien Di Mascio committed
243
244
        for objects in objdict.values():
            for obj in objects:
Sylvain Thénault's avatar
Sylvain Thénault committed
245
                objid = '%s_%s' % (reg, obj.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
246
                if objid in done:
247
                    break
Sylvain Thénault's avatar
Sylvain Thénault committed
248
249
250
251
252
                try: # XXX < 3.6 bw compat
                    pdefs = obj.property_defs
                except AttributeError:
                    pdefs = getattr(obj, 'cw_property_defs', {})
                if pdefs:
253
                    yield objid
Adrien Di Mascio's avatar
Adrien Di Mascio committed
254
                    done.add(objid)
255
                    break
256

257

Adrien Di Mascio's avatar
Adrien Di Mascio committed
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
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.
275

Adrien Di Mascio's avatar
Adrien Di Mascio committed
276
277
278
    It will regenerate cubicweb/i18n/xx.po files. You'll have then to edit those
    files to add translations of newly added messages.
    """
279
    name = 'i18ncubicweb'
280
    min_args = max_args = 0
Adrien Di Mascio's avatar
Adrien Di Mascio committed
281
282
283
284

    def run(self, args):
        """run the command with its specific arguments"""
        import shutil
285
        import tempfile
Adrien Di Mascio's avatar
Adrien Di Mascio committed
286
287
        import yams
        from logilab.common.fileutils import ensure_fs_mode
288
        from logilab.common.shellutils import globfind, find, rm
289
        from logilab.common.modutils import get_module_files
290
        from cubicweb.i18n import extract_from_tal, execute
291
        tempdir = tempfile.mkdtemp()
292
        cwi18ndir = WebConfiguration.i18n_lib_dir()
293
        print '-> extract schema messages.'
294
295
        schemapot = osp.join(tempdir, 'schema.pot')
        potfiles = [schemapot]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
296
297
298
299
300
301
        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()
302
        print '-> extract TAL messages.'
303
304
305
        tali18nfile = osp.join(tempdir, 'tali18n.py')
        extract_from_tal(find(osp.join(BASEDIR, 'web'), ('.py', '.pt')),
                         tali18nfile)
306
        print '-> generate .pot files.'
307
308
309
310
311
312
        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
313
314
                                ('yams', get_module_files(yams.__path__[0]), None),
                                ('tal', [tali18nfile], None),
315
                                ('js', jsfiles, 'java'),
Adrien Di Mascio's avatar
Adrien Di Mascio committed
316
                                ]:
317
            cmd = 'xgettext --no-location --omit-header -k_ -o %s %s'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
318
319
            if lang is not None:
                cmd += ' -L %s' % lang
320
            potfile = osp.join(tempdir, '%s.pot' % id)
321
            execute(cmd % (potfile, ' '.join('"%s"' % f for f in files)))
322
            if osp.exists(potfile):
323
324
                potfiles.append(potfile)
            else:
325
326
                print '-> WARNING: %s file was not generated' % potfile
        print '-> merging %i .pot files' % len(potfiles)
327
328
329
        cubicwebpot = osp.join(tempdir, 'cubicweb.pot')
        execute('msgcat -o %s %s'
                % (cubicwebpot, ' '.join('"%s"' % f for f in potfiles)))
330
        print '-> merging main pot file with existing translations.'
331
        chdir(cwi18ndir)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
332
        toedit = []
333
        for lang in CubicWebNoAppConfiguration.cw_languages():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
334
            target = '%s.po' % lang
335
336
            execute('msgmerge -N --sort-output -o "%snew" "%s" "%s"'
                    % (target, target, cubicwebpot))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
337
338
            ensure_fs_mode(target)
            shutil.move('%snew' % target, target)
339
            toedit.append(osp.abspath(target))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
340
        # cleanup
341
        rm(tempdir)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
342
        # instructions pour la suite
343
344
        print '-> regenerated CubicWeb\'s .po catalogs.'
        print '\nYou can now edit the following files:'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
345
        print '* ' + '\n* '.join(toedit)
346
        print 'when you are done, run "cubicweb-ctl i18ncube yourcube".'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
347
348


349
class UpdateCubeCatalogCommand(Command):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
350
351
352
    """Update i18n catalogs for cubes. If no cube is specified, update
    catalogs of all registered cubes.
    """
353
    name = 'i18ncube'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
354
    arguments = '[<cube>...]'
355

Adrien Di Mascio's avatar
Adrien Di Mascio committed
356
357
358
    def run(self, args):
        """run the command with its specific arguments"""
        if args:
359
            cubes = [DevConfiguration.cube_dir(cube) for cube in args]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
360
        else:
361
362
            cubes = [DevConfiguration.cube_dir(cube)
                     for cube in DevConfiguration.available_cubes()]
363
364
            cubes = [cubepath for cubepath in cubes
                     if osp.exists(osp.join(cubepath, 'i18n'))]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
365
366
        update_cubes_catalogs(cubes)

367

Adrien Di Mascio's avatar
Adrien Di Mascio committed
368
369
def update_cubes_catalogs(cubes):
    for cubedir in cubes:
370
        if not osp.isdir(cubedir):
371
            print '-> ignoring %s that is not a directory.' % cubedir
Adrien Di Mascio's avatar
Adrien Di Mascio committed
372
            continue
373
        try:
374
            toedit = update_cube_catalogs(cubedir)
375
376
377
        except Exception:
            import traceback
            traceback.print_exc()
Sylvain Thénault's avatar
Sylvain Thénault committed
378
379
380
            print '-> error while updating catalogs for cube', cubedir
        else:
            # instructions pour la suite
381
382
383
384
385
386
            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
387

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


463
464
465
466
467
468
469
# XXX totally broken, fix it
# class LiveServerCommand(Command):
#     """Run a server from within a cube directory.
#     """
#     name = 'live-server'
#     arguments = ''
#     options = ()
470

471
472
473
474
#     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
475
476


477
class NewCubeCommand(Command):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
478
479
480
    """Create a new cube.

    <cubename>
481
      the name of the new cube. It should be a valid python module name.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
482
483
484
    """
    name = 'newcube'
    arguments = '<cubename>'
485
    min_args = max_args = 1
486
    options = (
487
488
489
490
491
492
493
        ("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.',
          }
         ),
494
495
496
497
498
        ("directory",
         {'short': 'd', 'type' : 'string', 'metavar': '<cubes directory>',
          'help': 'directory where the new cube should be created',
          }
         ),
499
500
501
502
503
504
        ("verbose",
         {'short': 'v', 'type' : 'yn', 'metavar': '<verbose>',
          'default': 'n',
          'help': 'verbose mode: will ask all possible configuration questions',
          }
         ),
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
        ("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',
          }
         ),
523
524
525
526
527
528
        ("license",
         {'short': 'l', 'type' : 'choice', 'metavar': '<license>',
          'default': 'LGPL', 'choices': ('GPL', 'LGPL', ''),
          'help': 'cube license',
          }
         ),
529
530
        )

531
    LICENSES = {
532
533
534
535
536
537
538
539
540
541
542
543
544
545
        '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/>.
''',
546

547
548
549
550
551
552
553
554
555
556
557
558
559
560
        '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'
561
        }
562

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

631
    def _ask_for_dependencies(self):
632
633
        from logilab.common.shellutils import ASK
        from logilab.common.textutils import splitstrip
634
635
636
        depcubes = []
        for cube in ServerConfiguration.available_cubes():
            answer = ASK.ask("Depends on cube %s? " % cube,
637
638
                             ('N','y','skip','type'), 'N')
            if answer == 'y':
639
                depcubes.append(cube)
640
            if answer == 'type':
641
                depcubes = splitstrip(raw_input('type dependencies: '))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
642
                break
643
            elif answer == 'skip':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
644
                break
645
646
        return dict(('cubicweb-' + cube, ServerConfiguration.cube_version(cube))
                    for cube in depcubes)
647

648
649
650
651
652
653
654
655
656
657

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

658
659
    chances are the lines at the top are the ones that will bring the higher
    benefit after optimisation. Start there.
660
    """
661
    arguments = 'rql.log'
662
    name = 'exlog'
663
    options = ()
664

665
666
667
    def run(self, args):
        import re
        requests = {}
668
        for filepath in args:
669
            try:
670
671
672
673
                stream = file(filepath)
            except OSError, ex:
                raise BadCommandUsage("can't open rql log file %s: %s"
                                      % (filepath, ex))
674
            for lineno, line in enumerate(stream):
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
                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))
690
        stat = []
691
        for rql, times in requests.iteritems():
692
693
694
            stat.append( (sum(time[0] for time in times),
                          sum(time[1] for time in times),
                          len(times), rql) )
695
696
        stat.sort()
        stat.reverse()
697
        total_time = sum(clocktime for clocktime, cputime, occ, rql in stat) * 0.01
698
699
        print 'Percentage;Cumulative Time (clock);Cumulative Time (CPU);Occurences;Query'
        for clocktime, cputime, occ, rql in stat:
700
701
            print '%.2f;%.2f;%.2f;%s;%s' % (clocktime/total_time, clocktime,
                                            cputime, occ, rql)
702

703

704
705
706
707
class GenerateSchema(Command):
    """Generate schema image for the given cube"""
    name = "schema"
    arguments = '<cube>'
708
    min_args = max_args = 1
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
    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',
          }),
        ]
746
747

    def run(self, args):
748
749
        from subprocess import Popen
        from tempfile import NamedTemporaryFile
750
        from logilab.common.textutils import splitstrip
751
752
753
        from yams import schema2dot, BASE_TYPES
        from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
                                     WORKFLOW_TYPES, INTERNAL_TYPES)
754
        cubes = splitstrip(args[0])
755
        dev_conf = DevConfiguration(*cubes)
756
757
758
759
760
        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
761
762
763
764
765
766
767
768
769
770
        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)
771
772
773
774
        if viewer:
            p = Popen((viewer, out))
            p.wait()

775
776
777
778
779
780
781
782
783
784

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

785
for cmdcls in (UpdateCubicWebCatalogCommand,
786
               UpdateCubeCatalogCommand,
787
788
789
790
791
792
793
               #LiveServerCommand,
               NewCubeCommand,
               ExamineLogCommand,
               GenerateSchema,
               GenerateQUnitHTML,
               ):
    CWCTL.register(cmdcls)