Browse Source

empty selection in m2m field template

[FIX] other licenses, return lines as well

[FIX] license not shown in __oe__

[FIX] unprefix more names, try to get _name/_inherit right [IMP] group by module in zip

[FIX] fix category and summary being on same line

[FIX] fix export test

[IMP] add tabs for reports/security/workflow/data + partial data/demo generation

unprefix model names in __init__

[FIX] fix data file names in __openerp__.py

[IMP] move Data&Demo after Interface in view

[FIX] unprefix view file names

[IMP] remove prefixes from field attrs in views

[FIX] encode files in zip to utf-8, remove trailing comma in menu groups

remove unused variable in tests

remove AGPL3 or later from license choices: not in base module choices
12.0-mig-module_prototyper_last
Vincent Vinet 9 years ago
committed by Nicolas JEUDY
parent
commit
9cfa020594
  1. 1
      module_prototyper/README.rst
  2. 2
      module_prototyper/models/ir_model_fields.py
  3. 2
      module_prototyper/models/licenses.py
  4. 125
      module_prototyper/models/module_prototyper.py
  5. 9
      module_prototyper/templates/8.0/__openerp__.py.template
  6. 13
      module_prototyper/templates/8.0/data/model_name.xml.template
  7. 2
      module_prototyper/templates/8.0/models/__init__.py.template
  8. 23
      module_prototyper/templates/8.0/models/model_name.py.template
  9. 2
      module_prototyper/templates/8.0/views/model_menus.xml.template
  10. 5
      module_prototyper/tests/test_prototype_module_export.py
  11. 4
      module_prototyper/views/ir_model_fields_view.xml
  12. 40
      module_prototyper/views/module_prototyper_view.xml
  13. 50
      module_prototyper/wizard/module_prototyper_module_export.py

1
module_prototyper/README.rst

@ -65,6 +65,7 @@ Contributors
* Maxime Chambreuil <maxime.chambreuil@savoirfairelinux.com>
* El hadji Dem <elhadji.dem@savoirfairelinux.com>
* Savoir-faire Linux <support@savoirfairelinux.com>
* Vincent Vinet <vincent.vinet@savoirfairelinux.com>
Maintainer
----------

2
module_prototyper/models/ir_model_fields.py

@ -42,7 +42,7 @@ class ir_model_fields(models.Model):
"relation table"),
)
limit = fields.Integer('Read limit', help=_("Read limit"))
context = fields.Char(
client_context = fields.Char(
'Context',
help=_("Context to use on the client side when handling the field "
"(python dictionary)"),

2
module_prototyper/models/licenses.py

@ -68,6 +68,6 @@ def get_license_text(license):
name, version = GPL_LICENSES[license]
return BASE_GPL.format(name=name, version=version).splitlines()
elif license == OSI:
return BASE_OSI
return BASE_OSI.splitlines()
else:
return ""

125
module_prototyper/models/module_prototyper.py

@ -20,6 +20,7 @@
#
##############################################################################
import base64
import logging
import lxml.etree
import os
import re
@ -31,10 +32,13 @@ from datetime import date
from jinja2 import Environment, FileSystemLoader
from openerp import models, api, fields
from openerp.tools.safe_eval import safe_eval
from .default_description import get_default_description
from . import licenses
_logger = logging.getLogger(__name__)
class ModulePrototyper(models.Model):
"""Module Prototyper gathers different information from all over the
@ -53,7 +57,6 @@ class ModulePrototyper(models.Model):
(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')
],
@ -170,10 +173,29 @@ class ModulePrototyper(models.Model):
help=('Enter the list of record rules that you have created and '
'want to export in this module.')
)
report_ids = fields.Many2many(
'ir.actions.report.xml', 'prototype_report_rel',
'module_prototyper_id', 'report_id', 'Reports',
help=('Enter the list of reports that you have created and '
'want to export in this module.')
)
activity_ids = fields.Many2many(
'workflow.activity', 'prototype_wf_activity_rel',
'module_prototyper_id', 'activity_id', 'Activities',
help=('Enter the list of workflow activities that you have created '
'and want to export in this module')
)
transition_ids = fields.Many2many(
'workflow.transition', 'prototype_wf_transition_rel',
'module_prototyper_id', 'transition_id', 'Transitions',
help=('Enter the list of workflow transitions that you have created '
'and want to export in this module')
)
__data_files = []
__field_descriptions = {}
_env = None
_data_files = ()
_demo_files = ()
_field_descriptions = None
File_details = namedtuple('file_details', ['filename', 'filecontent'])
template_path = '{}/../templates/'.format(os.path.dirname(__file__))
@ -211,10 +233,8 @@ class ModulePrototyper(models.Model):
for attr_name in dir(field)
if not attr_name[0] == '_'
})
# custom fields start with the prefix x_.
# it has to be removed.
field_description['name'] = re.sub(r'^x_', '', field.name)
self.__field_descriptions[field] = field_description
field_description['name'] = self.unprefix(field.name)
self._field_descriptions[field] = field_description
@api.model
def generate_files(self):
@ -224,12 +244,17 @@ class ModulePrototyper(models.Model):
assert self._env is not None, \
'Run set_env(api_version) before to generate files.'
# Avoid sharing these across instances
self._data_files = []
self._demo_files = []
self._field_descriptions = {}
self.set_field_descriptions()
file_details = []
file_details.extend(self.generate_models_details())
file_details.extend(self.generate_views_details())
file_details.extend(self.generate_menus_details())
file_details.append(self.generate_module_init_file_details())
file_details.extend(self.generate_data_files())
# must be the last as the other generations might add information
# to put in the __openerp__: additional dependencies, views files, etc.
file_details.append(self.generate_module_openerp_file_details())
@ -262,7 +287,8 @@ class ModulePrototyper(models.Model):
'__openerp__.py',
'__openerp__.py.template',
prototype=self,
data_files=self.__data_files,
data_files=self._data_files,
demo_fiels=self._demo_files,
)
@api.model
@ -278,7 +304,8 @@ class ModulePrototyper(models.Model):
@api.model
def generate_models_details(self):
"""Finds the models from the list of fields and generates
"""
Finds the models from the list of fields and generates
the __init__ file and each models files (one by class).
"""
files = []
@ -291,7 +318,8 @@ class ModulePrototyper(models.Model):
# dependencies = set([dep.id for dep in self.dependencies])
relations = {}
for field in self.__field_descriptions.itervalues():
field_descriptions = self._field_descriptions or {}
for field in field_descriptions.itervalues():
model = field.get('model_id')
relations.setdefault(model, []).append(field)
# dependencies.add(model.id)
@ -329,7 +357,7 @@ class ModulePrototyper(models.Model):
views_details = []
for model, views in relations.iteritems():
filepath = 'views/{}_view.xml'.format(
self.friendly_name(model)
self.friendly_name(self.unprefix(model))
)
views_details.append(
self.generate_file_details(
@ -338,7 +366,7 @@ class ModulePrototyper(models.Model):
views=views
)
)
self.__data_files.append(filepath)
self._data_files.append(filepath)
return views_details
@ -348,13 +376,14 @@ class ModulePrototyper(models.Model):
relations = {}
for menu in self.menu_ids:
if menu.action and menu.action.res_model:
model = menu.action.res_model
model = self.unprefix(menu.action.res_model)
else:
model = 'ir_ui'
relations.setdefault(model, []).append(menu)
menus_details = []
for model_name, menus in relations.iteritems():
model_name = self.unprefix(model_name)
filepath = 'views/{}_menus.xml'.format(
self.friendly_name(model_name)
)
@ -365,7 +394,7 @@ class ModulePrototyper(models.Model):
menus=menus,
)
)
self.__data_files.append(filepath)
self._data_files.append(filepath)
return menus_details
@ -377,7 +406,7 @@ class ModulePrototyper(models.Model):
:param field_descriptions: list of ir.model.fields records.
:return: FileDetails instance.
"""
python_friendly_name = self.friendly_name(model.model)
python_friendly_name = self.friendly_name(self.unprefix(model.model))
return self.generate_file_details(
'models/{}.py'.format(python_friendly_name),
'models/model_name.py.template',
@ -386,22 +415,87 @@ class ModulePrototyper(models.Model):
fields=field_descriptions,
)
@api.model
def generate_data_files(self):
""" Generate data and demo files """
data, demo = {}, {}
filters = [
(data, ir_filter)
for ir_filter in self.data_ids
] + [
(demo, ir_filter)
for ir_filter in self.demo_ids
]
for target, ir_filter in filters:
model = ir_filter.model_id
model_obj = self.env[model]
target.setdefault(model, model_obj.browse([]))
target[model] |= model_obj.search(safe_eval(ir_filter.domain))
res = []
for prefix, model_data, file_list in [
('data', data, self._data_files),
('demo', demo, self._demo_files)]:
for model_name, records in model_data.iteritems():
fname = self.friendly_name(self.unprefix(model_name))
filename = '{0}/{1}.xml'.format(prefix, fname)
self._data_files.append(filename)
res.append(self.generate_file_details(
filename,
'data/model_name.xml.template',
model=model_name,
records=records,
))
return res
@classmethod
def unprefix(cls, name):
if not name:
return name
return re.sub('^x_', '', name)
@classmethod
def is_prefixed(cls, name):
return bool(re.match('^x_', name))
@classmethod
def friendly_name(cls, name):
return name.replace('.', '_')
@classmethod
def fixup_domain(cls, domain):
""" Fix a domain according to unprefixing of fields """
res = []
for elem in domain:
if len(elem) == 3:
elem = list(elem)
elem[0] = cls.unprefix(elem[0])
res.append(elem)
return res
@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("//*[@attrs]"):
try:
attrs = safe_eval(elem.attrib["attrs"])
except Exception:
_logger.error("Unable to eval attribute: %s, skipping",
elem.attrib["attrs"])
continue
if isinstance(attrs, dict):
for key, val in attrs.iteritems():
if isinstance(val, (list, tuple)):
attrs[key] = cls.fixup_domain(val)
elem.attrib["attrs"] = repr(attrs)
for elem in doc.xpath("//field"):
# Make fields self-closed by removing useless whitespace
if elem.text and not elem.text.strip():
@ -428,6 +522,7 @@ class ModulePrototyper(models.Model):
'cr': self._cr,
# Utility functions
'fixup_arch': self.fixup_arch,
'is_prefixed': self.is_prefixed,
'unprefix': self.unprefix,
'wrap': wrap,

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

@ -7,12 +7,13 @@
'author': '{{ prototype.author }}',
'maintainer': '{{ prototype.maintainer }}',
'website': '{{ prototype.website }}',
'license': '{{ prototype.licence }}',
'license': '{{ prototype.license }}',
# 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 # noqa
# for the full list
'category': '{{ prototype.with_context({}).category_id.name }}',{# In english please! #}
{# Use with_context({}) to get english category #}
'category': '{{ prototype.with_context({}).category_id.name }}',
'summary': '{{ prototype.summary }}',
'description': """
{{ prototype.description }}
@ -40,8 +41,8 @@
],
# only loaded in demonstration mode
'demo': [
{% for demo_file in prototype.demo_ids %}
'{{ demo_file.name }}',
{% for demo_file in demo_files %}
'{{ demo_file }}',
{% endfor %}
],

13
module_prototyper/templates/8.0/data/model_name.xml.template

@ -1,8 +1,17 @@
<?xml version="1.0"?>
<openerp>
<data>
{% for record in records %}
<!--
<record id="{{ model }}_{{ loop.index }}" model="{{ model }}">
{% for key, val in record.read()[0].items() %}
<field name="{{ key }}">{{ val }}</field>
{% endfor %}
</record>
-->
{% if not loop.last %}
{{ data }}
{% endif %}
{% endfor %}
</data>
</openerp>

2
module_prototyper/templates/8.0/models/__init__.py.template

@ -4,6 +4,6 @@
{% if loop.first %}
{% endif %}
from . import {{ model }}
from . import {{ unprefix(model) }}
{% endfor %}
{% endblock %}

23
module_prototyper/templates/8.0/models/model_name.py.template

@ -6,10 +6,10 @@ from openerp.tools.translate import _
class {{ unprefix(name) }}(models.Model):
{% if model.state == 'base' %}
_name = "{{ model.model }}"
{% if model.state == 'base' and not is_prefixed(model.model) %}
_inherit = "{{ unprefix(model.model) }}"
{% else %}
_inherit = "{{ model.model }}"
_name = "{{ unprefix(model.model) }}"
{% endif %}
{% if description %}
_description = "{{ description }}"
@ -26,10 +26,13 @@ class {{ unprefix(name) }}(models.Model):
{{ unprefix(field.name) }} = fields.{{ field.ttype|capitalize }}(
string=_("{{ field.field_description }}"),
{% if field.selection %}
selection={{ selection }},
selection={{ field.selection }},
{% endif %}
{% if field.relation %}
comodel_name="{{ field.relation }}",
comodel_name="{{ unprefix(field.relation) }}",
{% endif %}
{% if field.ttype == 'one2many' %}
inverse_name="{{ unprefix(field.relation_field) }}",
{% endif %}
{% if field.column1 %}
column1="{{ field.column1 }}",
@ -43,11 +46,13 @@ class {{ unprefix(name) }}(models.Model):
{% if field.size %}
size={{ field.size }},
{% endif %}
{% if field.domain %}
{% if field.ttype in ('many2one', 'many2many', 'one2many') %}
{% if field.domain %}
domain={{ field.domain }},
{% endif %}
{% if field.context %}
context={{ field.context }},
{% endif %}
{% if field.client_context %}
context={{ field.client_context }},
{% endif %}
{% endif %}
{% if field.limit %}
limit={{ field.limit }},

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

@ -19,7 +19,7 @@
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 %}"
groups="{% for group in menu.groups_id %}{{ group.get_xml_id(cr,1,1).values()[0] }}{% if not loop.last %},{% endif %}{% endfor %}"
/>
{% if not loop.last %}

5
module_prototyper/tests/test_prototype_module_export.py

@ -69,10 +69,7 @@ class test_prototype_module_export(common.TransactionCase):
def test_zip_files_returns_tuple(self):
"""Test the method return of the method that generate the zip file."""
file_details = (
('test.txt', 'generated'),
)
ret = self.main_model.zip_files(file_details)
ret = self.main_model.zip_files(self.exporter, [self.prototype])
self.assertIsInstance(ret, tuple)
self.assertIsInstance(
ret.zip_file, zipfile.ZipFile

4
module_prototyper/views/ir_model_fields_view.xml

@ -27,8 +27,8 @@
<field name="limit"
attrs="{'invisible': [('ttype', '!=', 'many2many')]}"
/>
<field name="context"
attrs="{'invisible': [('type', 'not in', ['many2one','one2many','many2many'])]}"
<field name="client_context"
attrs="{'invisible': [('ttype', 'not in', ['many2one','one2many','many2many'])]}"
/>
</field>
</field>

40
module_prototyper/views/module_prototyper_view.xml

@ -61,13 +61,6 @@
<page string="Dependencies">
<field name="dependency_ids"/>
</page>
<!--Not implemented yet-->
<!--<page string="Data &amp; Demo">-->
<!--<label for="data_ids"/>-->
<!--<field name="data_ids"/>-->
<!--<label for="demo_ids"/>-->
<!--<field name="demo_ids"/>-->
<!--</page>-->
<page string="Fields">
<label for="field_ids"/>
<field name="field_ids"/>
@ -78,15 +71,30 @@
<label for="menu_ids"/>
<field name="menu_ids"/>
</page>
<!--Not implemented yet-->
<!--<page string="Security">-->
<!--<label for="group_ids"/>-->
<!--<field name="group_ids"/>-->
<!--<label for="right_ids"/>-->
<!--<field name="right_ids"/>-->
<!--<label for="rule_ids"/>-->
<!--<field name="rule_ids"/>-->
<!--</page>-->
<page string="Data &amp; Demo">
<label for="data_ids"/>
<field name="data_ids"/>
<label for="demo_ids"/>
<field name="demo_ids"/>
</page>
<page string="Reports">
<label for="report_ids" />
<field name="report_ids" />
</page>
<page string="Security">
<label for="group_ids"/>
<field name="group_ids"/>
<label for="right_ids"/>
<field name="right_ids"/>
<label for="rule_ids"/>
<field name="rule_ids"/>
</page>
<page string="Workflows">
<label for="activity_ids" />
<field name="activity_ids" />
<label for="transition_ids" />
<field name="transition_ids" />
</page>
</notebook>
</sheet>
</form>

50
module_prototyper/wizard/module_prototyper_module_export.py

@ -19,10 +19,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import StringIO
import base64
import os
import zipfile
from collections import namedtuple
from openerp import fields, models, api
@ -66,24 +69,20 @@ class PrototypeModuleExport(models.TransientModel):
)
# getting the prototype of the wizard
prototype = self.env[active_model].browse(
self._context.get('active_id')
prototypes = self.env[active_model].browse(
[self._context.get('active_id')]
)
# setting the jinja environment.
# They will help the program to find the template to render the files
# with.
prototype.set_jinja_env(wizard.api_version)
zip_details = self.zip_files(wizard, prototypes)
# generate_files ask the prototype to investigate the input
# and to generate the file templates according to it.
# zip_files, in another hand, put all the template files into a package
# ready to be saved by the user.
zip_details = self.zip_files(prototype.generate_files())
if len(prototypes) == 1:
zip_name = prototypes[0].name
else:
zip_name = "prototyper_export"
wizard.write(
{
'name': '{}.zip'.format(prototype.name),
'name': '{}.zip'.format(zip_name),
'state': 'get',
'data': base64.encodestring(zip_details.stringIO.getvalue())
}
@ -100,7 +99,7 @@ class PrototypeModuleExport(models.TransientModel):
}
@staticmethod
def zip_files(file_details):
def zip_files(wizard, prototypes):
"""Takes a set of file and zips them.
:param file_details: tuple (filename, file_content)
:return: tuple (zip_file, stringIO)
@ -109,10 +108,25 @@ class PrototypeModuleExport(models.TransientModel):
out = StringIO.StringIO()
with zipfile.ZipFile(out, 'w') as target:
for filename, file_content in file_details:
info = zipfile.ZipInfo(filename)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 2175008768 # specifies mode 0644
target.writestr(info, file_content)
for prototype in prototypes:
# setting the jinja environment.
# They will help the program to find the template to render the
# files with.
prototype.set_jinja_env(wizard.api_version)
# generate_files ask the prototype to investigate the input and
# to generate the file templates according to it. zip_files,
# in another hand, put all the template files into a package
# ready to be saved by the user.
file_details = prototype.generate_files()
for filename, file_content in file_details:
if isinstance(file_content, unicode):
file_content = file_content.encode('utf-8')
# Prefix all names with module technical name
filename = os.path.join(prototype.name, filename)
info = zipfile.ZipInfo(filename)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 2175008768 # specifies mode 0644
target.writestr(info, file_content)
return zip_details(zip_file=target, stringIO=out)
Loading…
Cancel
Save