Browse Source

fix template description being indented

[FIX] Use human name and english category name for manifest

[IMP] multi line notes, unprefix names, pep8 py files

[IMP] whitespace cleanup in templates

[FIX] base models get _name, custom get _inherit

flake fix

add missing spaces

prevent crash on menus with no action

switch GPL-2 to LGPL-3

generate license headers with license name

fix pep/flake errors in generated files

add m2m and relation field options in fields
pull/107/head
Vincent Vinet 10 years ago
committed by Maxime Chambreuil
parent
commit
60f327b8d3
  1. 17
      module_prototyper/models/default_description.py
  2. 19
      module_prototyper/models/ir_model_fields.py
  3. 73
      module_prototyper/models/licenses.py
  4. 96
      module_prototyper/models/module_prototyper.py
  5. 3
      module_prototyper/templates/8.0/__init__.py.template
  6. 28
      module_prototyper/templates/8.0/__openerp__.py.template
  7. 32
      module_prototyper/templates/8.0/header.template
  8. 5
      module_prototyper/templates/8.0/models/__init__.py.template
  9. 61
      module_prototyper/templates/8.0/models/model_name.py.template
  10. 21
      module_prototyper/templates/8.0/views/model_menus.xml.template
  11. 16
      module_prototyper/templates/8.0/views/model_views.xml.template
  12. 5
      module_prototyper/tests/__init__.py
  13. 30
      module_prototyper/tests/test_prototype.py
  14. 16
      module_prototyper/views/ir_model_fields_view.xml

17
module_prototyper/models/default_description.py

@ -21,12 +21,7 @@
##############################################################################
def get_default_description(self):
"""
Extract the content of default description because the text is very huge
in module_prototyper model
"""
return """
DEFAULT_DESCRIPTION = """
Module name
===========
@ -69,7 +64,7 @@ def get_default_description(self):
Contributors
------------
* Firsname Lastname <email.address@example.org>
* Firstname Lastname <email.address@example.org>
Maintainer
----------
@ -85,3 +80,11 @@ def get_default_description(self):
promote its widespread use.
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

19
module_prototyper/models/ir_model_fields.py

@ -19,7 +19,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
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)"),
)

73
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
# (<http://www.savoirfairelinux.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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 <http://www.gnu.org/licenses/>.
"""
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 ""

96
module_prototyper/models/module_prototyper.py

@ -19,16 +19,21 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
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,
@ -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()

3
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 %}

28
module_prototyper/templates/8.0/__openerp__.py.template

@ -1,7 +1,8 @@
{% extends "header.template" %}
{% block body %}
{
'name': '{{ prototype.name }}',
'name': '{{ prototype.human_name }}',
'version': '{{ prototype.version }}',
'author': '{{ prototype.author }}',
'maintainer': '{{ prototype.maintainer }}',
@ -9,9 +10,9 @@
'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 }},
}

32
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 <http://www.gnu.org/licenses/>.
{% endif %}
{% endfor %}
{% endif %}
#
##############################################################################
{% block body %}{% endblock %}
{% block body %}
{% endblock %}

5
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 %}

61
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 %}

21
module_prototyper/templates/8.0/views/model_menus.xml.template

@ -1,24 +1,29 @@
<?xml version="1.0"?>
<openerp>
<data>
{% for menu in menus -%}
{% for menu in menus %}
<record id="action_{{ menu.action.name }}_{{ menu.action.view_type }}_view" model="{{ menu.action.type }}">
<field name="name">{{ menu.action.name }}</field>
<field name="name">{{ unprefix(menu.action.name) }}</field>
<field name="type">{{ menu.action.type }}</field>
<field name="res_model">{{ menu.action.res_model }}</field>
<field name="res_model">{{ unprefix(menu.action.res_model) }}</field>
<field name="view_type">{{ menu.action.view_type }}</field>
<field name="view_mode">{{ menu.action.view_mode }}</field>
{% if menu.action.help %}<field name="help" type="html">{{ menu.action.help }}
</field>{% endif %}
{% if menu.action.help %}
<field name="help" type="html">{{ menu.action.help }}
</field>
{% endif %}
</record>
<menuitem action="action_{{ menu.action.name }}_{{ menu.action.view_type }}_view"
<menuitem action="action_{{ unprefix(menu.action.name) }}_{{ menu.action.view_type }}_view"
name="{{ menu.name }}"
id="menu_action_{{ menu.name|replace('.', '_') }}_{{ menu.action.view_type }}"
id="menu_action_{{ unprefix(menu.name)|replace('.', '_') }}_{{ menu.action.view_type }}"
{% if menu.parent_id %}parent="{{ menu.parent_id.get_xml_id(cr,1,1).values()[0] }}"{% endif %}
sequence="{{ menu.sequence }}"
groups="{% for group in menu.groups_id %}{{ group.get_xml_id(cr,1,1).values()[0] }},{% endfor %}"
/>
{% endfor -%}
{% if not loop.last %}
{% endif %}
{% endfor %}
</data>
</openerp>

16
module_prototyper/templates/8.0/views/model_views.xml.template

@ -2,20 +2,14 @@
<openerp>
<data>
<!-- TODO: put here a reminder on what to do at the first edition -->
{% for view in views -%}
<record id="{{ view.name|replace('.', '_')}}_view" model="ir.ui.view">
<field name="name">{{ view.name }}.view</field>
<field name="model">{{ view.model }}</field>
{% for view in views %}
<record id="{{ unprefix(view.name)|replace('.', '_')}}_view" model="ir.ui.view">
<field name="name">{{ unprefix(view.name) }}.view</field>
<field name="model">{{ unprefix(view.model) }}</field>
<field name="view_type">{{ view.type }}</field>
<field name="inherit_id" ref="{{ view.inherit_id.get_xml_id(cr,1,1).values()[0] }}"/>
<field name="arch" type="xml">
{# 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("<?xml version='1.0'?>", "")|replace("'x_", "'")|replace("> </field>", "/>") }}
{{ fixup_arch(view.arch) }}
</field>
</record>
{% endfor %}

5
module_prototyper/tests/__init__.py

@ -23,8 +23,3 @@ from . import (
test_prototype_module_export,
test_prototype
)
checks = [
test_prototype_module_export,
test_prototype,
]

30
module_prototyper/tests/test_prototype.py

@ -17,8 +17,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
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(

16
module_prototyper/views/ir_model_fields_view.xml

@ -15,6 +15,22 @@
<field name="notes"
placeholder="Notes to help developers to understand the work or advanced features that should be added, ie: onchange, etc."/>
</field>
<field name="relation_field" position="after">
<field name="column1"
attrs="{'invisible': [('ttype', '!=', 'many2many')]}"
/>
<field name="column2"
attrs="{'invisible': [('ttype', '!=', 'many2many')]}"
/>
</field>
<field name="translate" position="after">
<field name="limit"
attrs="{'invisible': [('ttype', '!=', 'many2many')]}"
/>
<field name="context"
attrs="{'invisible': [('type', 'not in', ['many2one','one2many','many2many'])]}"
/>
</field>
</field>
</record>

Loading…
Cancel
Save