diff --git a/module_prototyper/README.rst b/module_prototyper/README.rst index 052438647..8c80dc740 100644 --- a/module_prototyper/README.rst +++ b/module_prototyper/README.rst @@ -65,6 +65,7 @@ Contributors * Maxime Chambreuil * El hadji Dem * Savoir-faire Linux +* Vincent Vinet Maintainer ---------- diff --git a/module_prototyper/models/ir_model_fields.py b/module_prototyper/models/ir_model_fields.py index 852c9960d..e67df14f8 100644 --- a/module_prototyper/models/ir_model_fields.py +++ b/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)"), diff --git a/module_prototyper/models/licenses.py b/module_prototyper/models/licenses.py index 3e667d973..075d27a59 100644 --- a/module_prototyper/models/licenses.py +++ b/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 "" diff --git a/module_prototyper/models/module_prototyper.py b/module_prototyper/models/module_prototyper.py index c83134159..25816ca87 100644 --- a/module_prototyper/models/module_prototyper.py +++ b/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, diff --git a/module_prototyper/templates/8.0/__openerp__.py.template b/module_prototyper/templates/8.0/__openerp__.py.template index 5c8e687ec..28f779704 100644 --- a/module_prototyper/templates/8.0/__openerp__.py.template +++ b/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 %} ], diff --git a/module_prototyper/templates/8.0/data/model_name.xml.template b/module_prototyper/templates/8.0/data/model_name.xml.template index 7b25f11f8..77335b07d 100644 --- a/module_prototyper/templates/8.0/data/model_name.xml.template +++ b/module_prototyper/templates/8.0/data/model_name.xml.template @@ -1,8 +1,17 @@ + {% for record in records %} + + {% if not loop.last %} - {{ data }} - + {% endif %} + {% endfor %} diff --git a/module_prototyper/templates/8.0/models/__init__.py.template b/module_prototyper/templates/8.0/models/__init__.py.template index 8e1a1083d..5513ba9b4 100644 --- a/module_prototyper/templates/8.0/models/__init__.py.template +++ b/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 %} 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 a02a60edb..44a0858d1 100644 --- a/module_prototyper/templates/8.0/models/model_name.py.template +++ b/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 }}, 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 e8067ecaa..77b327af4 100644 --- a/module_prototyper/templates/8.0/views/model_menus.xml.template +++ b/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 %} diff --git a/module_prototyper/tests/test_prototype_module_export.py b/module_prototyper/tests/test_prototype_module_export.py index 4953689c8..e58cd1b90 100644 --- a/module_prototyper/tests/test_prototype_module_export.py +++ b/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 diff --git a/module_prototyper/views/ir_model_fields_view.xml b/module_prototyper/views/ir_model_fields_view.xml index a0d28fc43..df2df0daf 100644 --- a/module_prototyper/views/ir_model_fields_view.xml +++ b/module_prototyper/views/ir_model_fields_view.xml @@ -27,8 +27,8 @@ - diff --git a/module_prototyper/views/module_prototyper_view.xml b/module_prototyper/views/module_prototyper_view.xml index eeb0b8e05..424fa7d61 100644 --- a/module_prototyper/views/module_prototyper_view.xml +++ b/module_prototyper/views/module_prototyper_view.xml @@ -61,13 +61,6 @@ - - - - - - - - - - - - - - - - + + + + + + + + diff --git a/module_prototyper/wizard/module_prototyper_module_export.py b/module_prototyper/wizard/module_prototyper_module_export.py index 16a679cd6..560bf591c 100644 --- a/module_prototyper/wizard/module_prototyper_module_export.py +++ b/module_prototyper/wizard/module_prototyper_module_export.py @@ -19,10 +19,13 @@ # along with this program. If not, see . # ############################################################################## + 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)