diff --git a/module_prototyper/models/default_description.py b/module_prototyper/models/default_description.py index d45224d7e..5263b7cd1 100644 --- a/module_prototyper/models/default_description.py +++ b/module_prototyper/models/default_description.py @@ -21,67 +21,70 @@ ############################################################################## -def get_default_description(self): - """ - Extract the content of default description because the text is very huge - in module_prototyper model - """ - return """ - Module name - =========== +DEFAULT_DESCRIPTION = """ +Module name +=========== + +This module was written to extend the functionality of ... to support ... +and allow you to ... - This module was written to extend the functionality of ... to support ... - and allow you to ... +Installation +============ - Installation - ============ +To install this module, you need to: - To install this module, you need to: + * do this ... - * do this ... +Configuration +============= - Configuration - ============= +To configure this module, you need to: - To configure this module, you need to: + * go to ... - * go to ... +Usage +===== - Usage - ===== +To use this module, you need to: - To use this module, you need to: + * go to ... - * go to ... +For further information, please visit: - For further information, please visit: + * https://www.odoo.com/forum/help-1 - * https://www.odoo.com/forum/help-1 +Known issues / Roadmap +====================== - Known issues / Roadmap - ====================== + * ... - * ... +Credits +======= - Credits - ======= +Contributors +------------ - Contributors - ------------ +* Firstname Lastname - * Firsname Lastname +Maintainer +---------- - Maintainer - ---------- +.. image:: http://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org - .. image:: http://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: http://odoo-community.org +This module is maintained by the OCA. - This module is maintained by the OCA. +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. - OCA, or the Odoo Community Association, is a nonprofit organization whose - mission is to support the collaborative development of Odoo features and - promote its widespread use. +To contribute to this module, please visit http://odoo-community.org.""" - To contribute to this module, please visit http://odoo-community.org.""" + +def get_default_description(self): + """ + Extract the content of default description because the text is very huge + in module_prototyper model + """ + return DEFAULT_DESCRIPTION diff --git a/module_prototyper/models/ir_model_fields.py b/module_prototyper/models/ir_model_fields.py index 80b9842af..852c9960d 100644 --- a/module_prototyper/models/ir_model_fields.py +++ b/module_prototyper/models/ir_model_fields.py @@ -19,7 +19,9 @@ # along with this program. If not, see . # ############################################################################## + from openerp import fields, models +from openerp.tools.translate import _ class ir_model_fields(models.Model): @@ -28,3 +30,20 @@ class ir_model_fields(models.Model): notes = fields.Text('Notes to developers.') helper = fields.Text('Helper') + # TODO: Make column1 and 2 required if a model has a m2m to itself + column1 = fields.Char( + 'Column1', + help=_("name of the column referring to 'these' records in the " + "relation table"), + ) + column2 = fields.Char( + 'Column2', + help=_("name of the column referring to 'those' records in the " + "relation table"), + ) + limit = fields.Integer('Read limit', help=_("Read limit")) + context = fields.Char( + 'Context', + help=_("Context to use on the client side when handling the field " + "(python dictionary)"), + ) diff --git a/module_prototyper/models/licenses.py b/module_prototyper/models/licenses.py new file mode 100644 index 000000000..3e667d973 --- /dev/null +++ b/module_prototyper/models/licenses.py @@ -0,0 +1,73 @@ +# -*- encoding: utf-8 -*- +# ############################################################################# +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2010 - 2014 Savoir-faire Linux +# (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +BASE_GPL = """ +This program is free software: you can redistribute it and/or modify +it under the terms of the {name} as +published by the Free Software Foundation{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 {name} +along with this program. If not, see . +""" + + +GPL3 = "GPL-3" +GPL3_L = "GPL-3 or any later version" +LGPL3 = "LGPL-3" +LGPL3_L = "LGPL-3 or any later version" +AGPL3 = "AGPL-3" +AGPL3_L = "AGPL-3 or any later version" +OSI = "Other OSI approved license" + +V3 = " version 3" +V3L = """, either version 3 of the +License, or (at your option) any later version""" + +GPL_LICENSES = { + GPL3: ("GNU General Public License", V3), + GPL3_L: ("GNU General Public License", V3L), + LGPL3: ("GNU Lesser General Public License", V3), + LGPL3_L: ("GNU Lesser General Public License", V3L), + AGPL3: ("GNU Affero General Public License", V3), + AGPL3_L: ("GNU Affero General Public License", V3L), +} + +BASE_OSI = """ +This program is free software: you should have received a copy of the +license under which it is distributed along with the program. +""" + + +def get_license_text(license): + """ Get the python license header for a license """ + if license in GPL_LICENSES: + name, version = GPL_LICENSES[license] + return BASE_GPL.format(name=name, version=version).splitlines() + elif license == OSI: + return BASE_OSI + else: + return "" diff --git a/module_prototyper/models/module_prototyper.py b/module_prototyper/models/module_prototyper.py index 9b9faa9a0..c83134159 100644 --- a/module_prototyper/models/module_prototyper.py +++ b/module_prototyper/models/module_prototyper.py @@ -19,16 +19,21 @@ # along with this program. If not, see . # ############################################################################## +import base64 +import lxml.etree import os import re -import base64 -from datetime import date +import textwrap from collections import namedtuple +from datetime import date + from jinja2 import Environment, FileSystemLoader + from openerp import models, api, fields + from .default_description import get_default_description -YEAR = date.today().year +from . import licenses class ModulePrototyper(models.Model): @@ -42,15 +47,18 @@ class ModulePrototyper(models.Model): _description = "Module Prototyper" license = fields.Selection( - [('GPL-2', 'GPL Version 2'), - ('GPL-2 or any later version', 'GPL-2 or later version'), - ('GPL-3', 'GPL Version 3'), - ('GPL-3 or any later version', 'GPL-3 or later version'), - ('AGPL-3', 'Affero GPL-3'), - ('Other OSI approved licence', 'Other OSI Approved Licence'), - ('Other proprietary', 'Other Proprietary')], + [ + (licenses.GPL3, 'GPL Version 3'), + (licenses.GPL3_L, 'GPL-3 or later version'), + (licenses.LGPL3, 'LGPL-3'), + (licenses.LGPL3_L, 'LGPL-3 or later version'), + (licenses.AGPL3, 'Affero GPL-3'), + (licenses.AGPL3_L, 'Affero GPL-3 or later version'), + (licenses.OSI, 'Other OSI Approved Licence'), + ('Other proprietary', 'Other Proprietary') + ], string='License', - default='AGPL-3', + default=licenses.AGPL3_L, ) name = fields.Char( 'Technical Name', required=True, @@ -68,15 +76,15 @@ class ModulePrototyper(models.Model): description = fields.Text( 'Description', required=True, - help=('Enter the description of your module, what it does, how to' - 'install, configure and use it, the roadmap or known issues.' + help=('Enter the description of your module, what it does, how to ' + 'install, configure and use it, the roadmap or known issues. ' 'The description will be exported in README.rst'), default=get_default_description ) author = fields.Char('Author', required=True, help=('Enter your name')) maintainer = fields.Char( 'Maintainer', - help=('Enter the name of the person or organization who will' + help=('Enter the name of the person or organization who will ' 'maintain this module') ) website = fields.Char('Website', help=('Enter the URL of your website')) @@ -106,7 +114,7 @@ class ModulePrototyper(models.Model): 'ir.module.module', 'module_prototyper_module_rel', 'module_prototyper_id', 'module_id', 'Dependencies', - help=('Enter the list of required modules that need to be installed' + help=('Enter the list of required modules that need to be installed ' 'for your module to work properly') ) data_ids = fields.Many2many( @@ -126,40 +134,40 @@ class ModulePrototyper(models.Model): field_ids = fields.Many2many( 'ir.model.fields', 'prototype_fields_rel', 'module_prototyper_id', 'field_id', 'Fields', - help=('Enter the list of fields that you have created or modified' - 'and want to export in this module. New models will be' + help=('Enter the list of fields that you have created or modified ' + 'and want to export in this module. New models will be ' 'exported as long as you choose one of his fields.') ) menu_ids = fields.Many2many( 'ir.ui.menu', 'prototype_menu_rel', 'module_prototyper_id', 'menu_id', 'Menu Items', - help=('Enter the list of menu items that you have created and want' - 'to export in this module. Related windows actions will be' + help=('Enter the list of menu items that you have created and want ' + 'to export in this module. Related windows actions will be ' 'exported as well.') ) view_ids = fields.Many2many( 'ir.ui.view', 'prototype_view_rel', 'module_prototyper_id', 'view_id', 'Views', - help=('Enter the list of views that you have created and want to' + help=('Enter the list of views that you have created and want to ' 'export in this module.') ) group_ids = fields.Many2many( 'res.groups', 'prototype_groups_rel', 'module_prototyper_id', 'group_id', 'Groups', - help=('Enter the list of groups that you have created and want to' + help=('Enter the list of groups that you have created and want to ' 'export in this module.') ) right_ids = fields.Many2many( 'ir.model.access', 'prototype_rights_rel', 'module_prototyper_id', 'right_id', 'Access Rights', - help=('Enter the list of access rights that you have created and' + help=('Enter the list of access rights that you have created and ' 'want to export in this module.') ) rule_ids = fields.Many2many( 'ir.rule', 'prototype_rule_rel', 'module_prototyper_id', 'rule_id', 'Record Rules', - help=('Enter the list of record rules that you have created and' + help=('Enter the list of record rules that you have created and ' 'want to export in this module.') ) @@ -178,6 +186,8 @@ class ModulePrototyper(models.Model): """ if self._env is None: self._env = Environment( + lstrip_blocks=True, + trim_blocks=True, loader=FileSystemLoader( os.path.join(self.template_path, api_version) ) @@ -337,7 +347,11 @@ class ModulePrototyper(models.Model): """Wrapper to generate the menus files.""" relations = {} for menu in self.menu_ids: - relations.setdefault(menu.action.res_model, []).append(menu) + if menu.action and menu.action.res_model: + model = menu.action.res_model + else: + model = 'ir_ui' + relations.setdefault(model, []).append(menu) menus_details = [] for model_name, menus in relations.iteritems(): @@ -368,14 +382,33 @@ class ModulePrototyper(models.Model): 'models/{}.py'.format(python_friendly_name), 'models/model_name.py.template', name=python_friendly_name, - inherit=model.model, + model=model, fields=field_descriptions, ) - @staticmethod - def friendly_name(name): + @classmethod + def unprefix(cls, name): + if not name: + return name + return re.sub('^x_', '', name) + + @classmethod + def friendly_name(cls, name): return name.replace('.', '_') + @classmethod + def fixup_arch(cls, archstr): + doc = lxml.etree.fromstring(archstr) + for elem in doc.xpath("//*[@name]"): + elem.attrib["name"] = cls.unprefix(elem.attrib["name"]) + + for elem in doc.xpath("//field"): + # Make fields self-closed by removing useless whitespace + if elem.text and not elem.text.strip(): + elem.text = None + + return lxml.etree.tostring(doc) + @api.model def generate_file_details(self, filename, template, **kwargs): """ generate file details from jinja2 template. @@ -388,10 +421,41 @@ class ModulePrototyper(models.Model): # keywords used in several templates. kwargs.update( { - 'export_year': YEAR, + 'export_year': date.today().year, 'author': self.author, 'website': self.website, + 'license_text': licenses.get_license_text(self.license), 'cr': self._cr, + # Utility functions + 'fixup_arch': self.fixup_arch, + 'unprefix': self.unprefix, + 'wrap': wrap, + } ) return self.File_details(filename, template.render(kwargs)) + + +# Utility functions for rendering templates +def wrap(text, **kwargs): + """ Wrap some text for inclusion in a template, returning lines + + keyword arguments available, from textwrap.TextWrapper: + + width=70 + initial_indent='' + subsequent_indent='' + expand_tabs=True + replace_whitespace=True + fix_sentence_endings=False + break_long_words=True + drop_whitespace=True + break_on_hyphens=True + """ + if not text: + return [] + wrapper = textwrap.TextWrapper(**kwargs) + # We join the lines and split them again to offer a stable api for + # the jinja2 templates, regardless of replace_whitspace=True|False + text = "\n".join(wrapper.wrap(text)) + return text.splitlines() diff --git a/module_prototyper/templates/8.0/__init__.py.template b/module_prototyper/templates/8.0/__init__.py.template index 82452828d..384a9cd45 100644 --- a/module_prototyper/templates/8.0/__init__.py.template +++ b/module_prototyper/templates/8.0/__init__.py.template @@ -1,6 +1,7 @@ {% extends "header.template" %} {% block body %} -{% if models -%} +{% if models %} + from . import models {% endif %} {% endblock %} diff --git a/module_prototyper/templates/8.0/__openerp__.py.template b/module_prototyper/templates/8.0/__openerp__.py.template index 23d1125cb..5c8e687ec 100644 --- a/module_prototyper/templates/8.0/__openerp__.py.template +++ b/module_prototyper/templates/8.0/__openerp__.py.template @@ -1,17 +1,18 @@ {% extends "header.template" %} {% block body %} + { - 'name': '{{ prototype.name }}', + 'name': '{{ prototype.human_name }}', 'version': '{{ prototype.version }}', 'author': '{{ prototype.author }}', 'maintainer': '{{ prototype.maintainer }}', 'website': '{{ prototype.website }}', 'license': '{{ prototype.licence }}', - + # Categories can be used to filter modules in modules listing - # Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml + # Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml # noqa # for the full list - 'category': '{{ prototype.category_id.name }}', + 'category': '{{ prototype.with_context({}).category_id.name }}',{# In english please! #} 'summary': '{{ prototype.summary }}', 'description': """ {{ prototype.description }} @@ -23,33 +24,36 @@ # any module necessary for this one to work correctly 'depends': [ - {% for dependency in prototype.dependency_ids -%} + {% for dependency in prototype.dependency_ids %} '{{ dependency.name }}', - {% endfor -%}], + {% endfor %} + ], 'external_dependencies': { 'python': [], }, - + # always loaded 'data': [ - {% for data_file in data_files -%} + {% for data_file in data_files %} '{{ data_file }}', - {% endfor -%}], + {% endfor %} + ], # only loaded in demonstration mode 'demo': [ - {% for demo_file in prototype.demo_ids -%} + {% for demo_file in prototype.demo_ids %} '{{ demo_file.name }}', - {% endfor -%}], - + {% endfor %} + ], + # used for Javascript Web CLient Testing with QUnit / PhantomJS - # https://www.odoo.com/documentation/8.0/reference/javascript.html#testing-in-odoo-web-client + # https://www.odoo.com/documentation/8.0/reference/javascript.html#testing-in-odoo-web-client # noqa 'js': [], 'css': [], 'qweb': [], 'installable': True, - # Install this module automatically if all dependency have been previously and independently installed. - # Used for synergetic or glue modules. + # Install this module automatically if all dependency have been previously + # and independently installed. Used for synergetic or glue modules. 'auto_install': {{ prototype.auto_install }}, 'application': {{ prototype.application }}, } diff --git a/module_prototyper/templates/8.0/header.template b/module_prototyper/templates/8.0/header.template index 2e6251e7b..295d4d817 100644 --- a/module_prototyper/templates/8.0/header.template +++ b/module_prototyper/templates/8.0/header.template @@ -2,21 +2,25 @@ ############################################################################## # # Odoo, Open Source Management Solution -# This module copyright (C) {{ export_year }} {% if author %}{{ author }}{% endif %} -# {% if website %}({{ website }}).{% endif %} +{% if author %} +# This module copyright (C) {{ export_year }} {{ author }} +{% else %} +# This module copyright (C) {{ export_year }} +{% endif %} +{% if website %} +# ({{ website }}). +{% endif %} +{% if license_text %} # -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. +{% for line in license_text %} +{% if line.strip() %} +# {{ line }} +{% else %} # -# 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . +{% endif %} +{% endfor %} +{% endif %} # ############################################################################## -{% block body %}{% endblock %} \ No newline at end of file +{% block body %} +{% endblock %} diff --git a/module_prototyper/templates/8.0/models/__init__.py.template b/module_prototyper/templates/8.0/models/__init__.py.template index caf9d7c74..8e1a1083d 100644 --- a/module_prototyper/templates/8.0/models/__init__.py.template +++ b/module_prototyper/templates/8.0/models/__init__.py.template @@ -1,6 +1,9 @@ {% extends "header.template" %} {% block body %} -{% for model in models -%} +{% for model in models %} +{% if loop.first %} + +{% endif %} from . import {{ model }} {% endfor %} {% endblock %} diff --git a/module_prototyper/templates/8.0/models/model_name.py.template b/module_prototyper/templates/8.0/models/model_name.py.template index 8f8ff5378..a02a60edb 100644 --- a/module_prototyper/templates/8.0/models/model_name.py.template +++ b/module_prototyper/templates/8.0/models/model_name.py.template @@ -1,22 +1,63 @@ {% extends "header.template" %} {% block body %} + from openerp import models, fields from openerp.tools.translate import _ -class {{ name }}(models.Model): - _inherit = "{{ inherit }}" - {% if description -%}_description = "{{ description }}"{% endif %} +class {{ unprefix(name) }}(models.Model): + {% if model.state == 'base' %} + _name = "{{ model.model }}" + {% else %} + _inherit = "{{ model.model }}" + {% endif %} + {% if description %} + _description = "{{ description }}" + {% endif %} - {% for field in fields -%} - {% if field.notes -%}# {{ field.notes }}{% endif %} - {{ field.name }} = fields.{{ field.ttype|capitalize }}( + {% for field in fields %} + {% for line in wrap(field.notes, replace_whitespace=False) %} + {% if line %} + # {{line}} + {% else %} + # + {% endif %} + {% endfor %} + {{ unprefix(field.name) }} = fields.{{ field.ttype|capitalize }}( string=_("{{ field.field_description }}"), + {% if field.selection %} + selection={{ selection }}, + {% endif %} + {% if field.relation %} + comodel_name="{{ field.relation }}", + {% endif %} + {% if field.column1 %} + column1="{{ field.column1 }}", + {% endif %} + {% if field.column2 %} + column1="{{ field.column2 }}", + {% endif %} required={{ field.required }}, translate={{ field.translate }}, readonly={{ field.readonly }}, - {% if field.size -%}size={{ field.size }},{% endif %} - {% if field.helper -%}help=_("{{ field.helper }}"),{% endif %} - ){% endfor %} - + {% if field.size %} + size={{ field.size }}, + {% endif %} + {% if field.domain %} + domain={{ field.domain }}, + {% endif %} + {% if field.context %} + context={{ field.context }}, + {% endif %} + {% if field.limit %} + limit={{ field.limit }}, + {% endif %} + {% if field.ttype == 'many2one' and field.on_delete %} + on_delete="{{ field.on_delete }}", + {% endif %} + {% if field.helper %} + help=_("{{ field.helper }}"), + {% endif %} + ) + {% endfor %} {% endblock %} diff --git a/module_prototyper/templates/8.0/views/model_menus.xml.template b/module_prototyper/templates/8.0/views/model_menus.xml.template index 642a682d8..e8067ecaa 100644 --- a/module_prototyper/templates/8.0/views/model_menus.xml.template +++ b/module_prototyper/templates/8.0/views/model_menus.xml.template @@ -1,24 +1,29 @@ - {% for menu in menus -%} - - {{ menu.action.name }} - {{ menu.action.type }} - {{ menu.action.res_model }} - {{ menu.action.view_type }} - {{ menu.action.view_mode }} - {% if menu.action.help %}{{ menu.action.help }} - {% endif %} - + {% for menu in menus %} + + {{ unprefix(menu.action.name) }} + {{ menu.action.type }} + {{ unprefix(menu.action.res_model) }} + {{ menu.action.view_type }} + {{ menu.action.view_mode }} + {% if menu.action.help %} + {{ menu.action.help }} + + {% endif %} + - - {% endfor -%} + + {% if not loop.last %} + + {% endif %} + {% endfor %} diff --git a/module_prototyper/templates/8.0/views/model_views.xml.template b/module_prototyper/templates/8.0/views/model_views.xml.template index b919abea7..218ec67df 100644 --- a/module_prototyper/templates/8.0/views/model_views.xml.template +++ b/module_prototyper/templates/8.0/views/model_views.xml.template @@ -2,22 +2,16 @@ - - {% for view in views -%} - - {{ view.name }}.view - {{ view.model }} - {{ view.type }} - - - {# the arch given by odoo start with an xml tag that - will break the xml tree we build. - Be careful, custom field have a x_ prefix that has to be - removed - #} - {{ view.arch|replace('"', "'")|replace("", "")|replace("'x_", "'")|replace("> ", "/>") }} - - + {% for view in views %} + + {{ unprefix(view.name) }}.view + {{ unprefix(view.model) }} + {{ view.type }} + + + {{ fixup_arch(view.arch) }} + + {% endfor %} diff --git a/module_prototyper/tests/__init__.py b/module_prototyper/tests/__init__.py index cb80f7016..aff3c1c83 100644 --- a/module_prototyper/tests/__init__.py +++ b/module_prototyper/tests/__init__.py @@ -23,8 +23,3 @@ from . import ( test_prototype_module_export, test_prototype ) - -checks = [ - test_prototype_module_export, - test_prototype, -] diff --git a/module_prototyper/tests/test_prototype.py b/module_prototyper/tests/test_prototype.py index 0ffdc0707..76d3c8fdc 100644 --- a/module_prototyper/tests/test_prototype.py +++ b/module_prototyper/tests/test_prototype.py @@ -17,8 +17,17 @@ # along with this program. If not, see . # +import ast +import lxml.etree + +try: + import pep8 +except ImportError: + pep8 = None + from jinja2 import Environment from jinja2.exceptions import TemplateNotFound + from openerp.tests import common @@ -59,6 +68,27 @@ class TestModulePrototyper(common.TransactionCase): self.assertIsInstance(file_details.filename, basestring) self.assertIsInstance(file_details.filecontent, basestring) + name, contents = file_details + if name.endswith(".py"): + # We have a "coding utf-8" line in there, we need to encode + contents = contents.encode("utf-8") + ast.parse(contents) + if pep8: + checker = pep8.Checker( + name, + contents.splitlines(True)) + res = checker.check_all() + self.assertFalse( + res, + "Python file {0} has pep8 errors:\n" + "{1}\n{2}".format(name, checker.report.messages, + repr(contents)) + ) + + elif name.endswith(".xml"): + # TODO validate valid odoo xml + lxml.etree.fromstring(contents) + def test_generate_files_raise_templatenotfound_if_not_found(self): self.prototype.set_jinja_env('t_api_version') self.assertRaises( diff --git a/module_prototyper/views/ir_model_fields_view.xml b/module_prototyper/views/ir_model_fields_view.xml index 39e659900..a0d28fc43 100644 --- a/module_prototyper/views/ir_model_fields_view.xml +++ b/module_prototyper/views/ir_model_fields_view.xml @@ -15,6 +15,22 @@ + + + + + + + +