[ADD] doc: new documentation, with training tutorials, and new scaffolding
[odoo/odoo.git] / openerp / cli / scaffold.py
index 5999aad..c4f2c44 100644 (file)
@@ -1,13 +1,8 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 import argparse
-import ast
-import functools
-import keyword
-import logging
 import os
 import re
-import simplejson
 import sys
 
 import jinja2
@@ -18,199 +13,43 @@ from openerp.modules.module import (get_module_root, MANIFEST, load_information_
 
 
 class Scaffold(Command):
-    "Generate an Odoo module skeleton."
+    """ Generates an Odoo module skeleton. """
 
     def run(self, cmdargs):
         # TODO: bash completion file
         parser = argparse.ArgumentParser(
             prog="%s scaffold" % sys.argv[0].split(os.path.sep)[-1],
-            description=self.__doc__
+            description=self.__doc__,
+            epilog=self.epilog(),
         )
-        parser.add_argument('--init', type=identifier, help='Initialize a new Odoo module')
-
-        parser.add_argument('--dest', default=".",
-            help='Directory where the module should be created/updated (default to current directory)')
-
-        parser.add_argument('--model', type=identifier, help="Name of the model to add")
-
-        parser.add_argument('--controller', type=identifier, help="Name of the controller to add")
-
-        parser.add_argument('--web', action='store_true', default=False,
-                         help="Generate structure for a webclient module")
-
-        parser.add_argument('--theme', action='store_true', default=False,
-                         help="Generate structure for a Website theme")
+        parser.add_argument(
+            '-t', '--template', type=template, default=template('default'),
+            help="Use a custom module template, can be a template name or the"
+                 " path to a module template (default: %(default)s)")
+        parser.add_argument('name', help="Name of the module to create")
+        parser.add_argument(
+            'dest', default='.', nargs='?',
+            help="Directory to create the module in (default: %(default)s)")
 
         if not cmdargs:
             sys.exit(parser.print_help())
         args = parser.parse_args(args=cmdargs)
 
-        dest = directory(args.dest)
-        if args.init:
-            dest = os.path.join(dest, args.init)
-            if os.path.exists(dest):
-                die("Can't initialize module in `%s`: Directory already exists." % dest)
-            if get_module_root(dest):
-                die("Can't init a new module in another Odoo module, you probably want to run this "
-                    "command from your project's root")
-        else:
-             mroot = get_module_root(dest)
-             if not mroot:
-                 die("The path `%s` provided does not point to an existing Odoo module. "
-                     "Forgot to `--init` ?" % dest)
-             dest = mroot
-
-        logging.disable(logging.CRITICAL)
-        scaffold = ScaffoldModule(dest)
-        if args.model:
-            scaffold.add_model(snake(args.model))
-        if args.controller:
-            scaffold.add_controller(args.controller)
-        if args.web:
-            scaffold.add_webclient_structure()
-        if args.theme:
-            scaffold.add_theme_structure()
-
-class ScaffoldModule(object):
-    """
-    Object for scaffolding existing or new Odoo modules
+        args.template.render_to(
+            snake(args.name),
+            directory(args.dest, create=True),
+            {'name': args.name})
 
-    @param path: Path of an existing module or path of module to create
-    """
-    def __init__(self, path):
-        env = jinja2.Environment(loader=jinja2.PackageLoader(
-            'openerp.cli', 'scaffold'), keep_trailing_newline=True)
-        env.filters['snake'] = snake
-        self.env = env
-        self.path = functools.partial(os.path.join, directory(path))
-        self.created = not os.path.exists(self.path())
-        directory(path, create=True)
-        self.module = self.path().split(os.path.sep)[-1]
-        if self.created:
-            manifest_base = os.path.splitext(MANIFEST)[0]
-            self.render_file('%s.jinja2' % manifest_base, self.path('%s.py' % manifest_base))
-            # Create an empty __init__.py so the module can be imported
-            open(self.path('__init__.py'), 'a').close()
-
-    def add_model(self, model):
-        model_module = snake(model)
-        model_file = self.path('models', '%s.py' % model_module)
-        if os.path.exists(model_file):
-            die("Model `%s` already exists !" % model_file)
-        self.add_init_import(self.path('__init__.py'), 'models')
-        self.add_init_import(self.path('models', '__init__.py'), model_module)
-
-        self.render_file('models.jinja2', model_file, model=model)
-        self.render_file('ir.model.access.jinja2', self.path('security', 'ir.model.access.csv'),
-                         if_exists='append', model=model)
-        self.append_manifest_list('data', 'security/ir.model.access.csv')
-
-        demo_file = 'data/%s_demo.xml' % self.module
-        self.append_xml_data('record.jinja2', self.path(demo_file),
-                             model=model)
-        self.append_manifest_list('demo', demo_file)
-
-    def add_controller(self, controller):
-        controller_module = snake(controller)
-        controller_file = self.path('controllers', '%s.py' % controller_module)
-        if os.path.exists(controller_file):
-            die("Controller `%s` already exists !" % controller_file)
-        self.add_init_import(self.path('__init__.py'), 'controllers')
-        # Check if the controller name correspond to a model and expose result to templates
-        has_model = self.has_import(self.path('models', '__init__.py'), controller_module)
-        self.add_init_import(self.path('controllers', '__init__.py'), controller_module)
-        self.render_file('controllers.jinja2', controller_file, controller=controller,
-                         has_model=has_model)
-
-    def add_webclient_structure(self):
-        self.append_manifest_list('depends', 'web')
-        prefix = '%s.%%s' % self.module
-        for ext in ('js', 'css', 'xml'):
-            self.render_file('webclient_%s.jinja2' % ext,
-                             self.path('static', 'src', ext, prefix % ext))
-
-    def add_theme_structure(self):
-        self.append_manifest_list('depends', 'website')
-        css_file = '%s_theme.css' % self.module
-        self.render_file('theme_css.jinja2', self.path('static', 'src', 'css', css_file))
-        self.append_xml_data('theme_xml.jinja2', self.path('views', 'templates.xml'), skip_if_exist=True)
-        self.append_manifest_list('data', 'views/templates.xml')
-
-    def has_import(self, initfile, module):
-        if not os.path.isfile(initfile):
-            return False
-        with open(initfile, 'r') as f:
-            for imp in ast.parse(f.read()).body:
-                if isinstance(imp, ast.Import):
-                    if module in [mod.name for mod in imp.names]:
-                        return True
-        return False
-
-    def get_manifest(self, key=None, default=None):
-        manifest = load_manifest(self.module, self.path())
-        if key:
-            return manifest.get(key, default)
-        else:
-            return manifest
-
-    def append_manifest_list(self, key, value, unique=True):
-        # TODO: append value without altering serialized formatting
-        vals = self.get_manifest(key, [])
-        if unique and value in vals:
-            return
-        vals.append(value)
-        self.change_manifest_key(key, vals)
-
-    def change_manifest_key(self, key, value):
-        value = simplejson.dumps(value)
-        with open(self.path(MANIFEST), 'r') as f:
-            data = f.read()
-        sdata = re.split('["\']%s["\']\s?:\s?\[[^\]]*\]' % key, data)
-        add = "'%s': %s" % (key, value)
-        if len(sdata) != 2:
-            warn("Could not update `%s` key in manifest. You should add this by yourself:"
-                 "\n\n%s\n" % (key, add))
-        else:
-            with open(self.path(MANIFEST), 'w') as f:
-                f.write(add.join(sdata))
-
-    def add_init_import(self, initfile, module):
-        if not(os.path.exists(initfile) and self.has_import(initfile, module)):
-            self.render_file('__init__.jinja2', initfile, if_exists='append', modules=[module])
-
-    def render_file(self, template, dest, if_exists='skip', **kwargs):
-        mode = 'a'
-        if os.path.exists(dest):
-            if if_exists == 'replace':
-                mode = 'w'
-            elif if_exists != 'append':
-                warn("File `%s` already exists. Skipping it..." % dest)
-                return
-        else:
-            kwargs['file_created'] = True
-        outdir = os.path.dirname(dest)
-        if not os.path.exists(outdir):
-            os.makedirs(outdir)
-        content = self.env.get_template(template).render(module=self.module, **kwargs)
-        with open(dest, mode) as f:
-            f.write(content)
-
-    def append_xml_data(self, template, dest, skip_if_exist=False, **kwargs):
-        if not os.path.exists(dest):
-            self.render_file('xmldata.jinja2', dest, **kwargs)
-        elif skip_if_exist:
-            warn("File `%s` already exists. Skipping it..." % dest)
-        with open(dest, 'r') as f:
-            data = f.read()
-        m = re.search('(^\s*)?</data>', data, re.MULTILINE)
-        content = self.env.get_template(template).render(module=self.module, **kwargs)
-        if not m:
-            warn("Could not add data in `%s`. You should add this by yourself:"
-                 "\n\n%s\n" % (dest, content))
-        else:
-            data = data[:m.start()] + content + m.group() + data[m.end():]
-            with open(self.path(dest), 'w') as f:
-                f.write(data)
+    def epilog(self):
+        return "Built-in templates available are: %s" % ', '.join(
+            d for d in os.listdir(builtins())
+            if d != 'base'
+        )
+
+builtins = lambda *args: os.path.join(
+    os.path.abspath(os.path.dirname(__file__)),
+    'templates',
+    *args)
 
 def snake(s):
     """ snake cases ``s``
@@ -223,13 +62,11 @@ def snake(s):
     s = re.sub(r'(?<=[^A-Z])\B([A-Z])', r' \1', s)
     # lowercase everything, split on whitespace and join
     return '_'.join(s.lower().split())
-
-def identifier(s):
-    if keyword.iskeyword(s):
-        die("%s is a Python keyword and can not be used as a name" % s)
-    if not re.match('[A-Za-z_][A-Za-z0-9_]*', s):
-        die("%s is not a valid Python identifier" % s)
-    return s
+def pascal(s):
+    return ''.join(
+        ss.capitalize()
+        for ss in re.sub('[_\s]+', ' ', s).split()
+    )
 
 def directory(p, create=False):
     expanded = os.path.abspath(
@@ -237,10 +74,57 @@ def directory(p, create=False):
             os.path.expandvars(p)))
     if create and not os.path.exists(expanded):
         os.makedirs(expanded)
-    if create and not os.path.isdir(expanded):
-        die("%s exists but is not a directory" % p)
+    if not os.path.isdir(expanded):
+        die("%s is not a directory" % p)
     return expanded
 
+env = jinja2.Environment()
+env.filters['snake'] = snake
+env.filters['pascal'] = pascal
+class template(object):
+    def __init__(self, identifier):
+        # TODO: directories, archives (zipfile, tarfile)
+        self.id = identifier
+        if not os.path.isdir(self.path):
+            die("{} is not a valid module template".format(identifier))
+
+    def __str__(self):
+        return self.id
+
+    @property
+    def path(self):
+        return builtins(self.id)
+
+    def files(self):
+        """ Lists the (local) path and content of all files in the template
+        """
+        for root, _, files in os.walk(self.path):
+            for f in files:
+                path = os.path.join(root, f)
+                yield path, open(path, 'rb').read()
+
+    def render_to(self, modname, directory, params=None):
+        """ Render this module template to ``dest`` with the provided
+         rendering parameters
+        """
+        # overwrite with local
+        for path, content in self.files():
+            _, ext = os.path.splitext(path)
+
+            local = os.path.relpath(path, self.path)
+            dest = os.path.join(directory, modname, local)
+            destdir = os.path.dirname(dest)
+            if not os.path.exists(destdir):
+                os.makedirs(destdir)
+
+            with open(dest, 'wb') as f:
+                if ext not in ('.py', '.xml', '.csv', '.js'):
+                    f.write(content)
+                else:
+                    env.from_string(content)\
+                       .stream(params or {})\
+                       .dump(f, encoding='utf-8')
+
 def die(message, code=1):
     print >>sys.stderr, message
     sys.exit(code)