You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

186 lines
6.4 KiB

  1. # Copyright (C) 2019-Today: GRAP (<http://www.grap.coop/>)
  2. # @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. import os
  5. import pygount
  6. from pathlib import Path
  7. import logging
  8. from odoo import api, fields, models
  9. from odoo.tools.safe_eval import safe_eval
  10. from odoo.modules.module import get_module_path
  11. _logger = logging.getLogger(__name__)
  12. class IrModuleModule(models.Model):
  13. _inherit = 'ir.module.module'
  14. author_ids = fields.Many2many(
  15. string='Authors', comodel_name='ir.module.author', readonly=True,
  16. relation='ir_module_module_author_rel')
  17. module_type_id = fields.Many2one(
  18. string='Module Type', comodel_name='ir.module.type', readonly=True)
  19. python_code_qty = fields.Integer(
  20. string='Python Code Quantity', readonly=True)
  21. xml_code_qty = fields.Integer(
  22. string='XML Code Quantity', readonly=True)
  23. js_code_qty = fields.Integer(
  24. string='JS Code Quantity', readonly=True)
  25. css_code_qty = fields.Integer(
  26. string='CSS Code Quantity', readonly=True)
  27. # Overloadable Section
  28. @api.model
  29. def _get_analyse_settings(self):
  30. """Return a dictionnary of data analysed
  31. Overload this function if you want to analyse other data
  32. {
  33. 'extension': {
  34. 'data_to_analyse': 'field_name',
  35. },
  36. }, [...]
  37. extension: extension of the file, with the '.'
  38. data_to_analyse : possible value : code, documentation, empty, string
  39. field_name: Odoo field name to store the analysis
  40. """
  41. return {
  42. '.py': {
  43. 'code': 'python_code_qty',
  44. },
  45. '.xml': {
  46. 'code': 'xml_code_qty',
  47. },
  48. '.js': {
  49. 'code': 'js_code_qty',
  50. },
  51. '.css': {
  52. 'code': 'css_code_qty',
  53. },
  54. }
  55. @api.model
  56. def _get_clean_analyse_values(self):
  57. """List of fields to unset when a module is uninstalled"""
  58. return {
  59. 'author_ids': [6, 0, []],
  60. 'module_type_id': False,
  61. 'python_code_qty': False,
  62. 'xml_code_qty': 0,
  63. 'js_code_qty': 0,
  64. 'css_code_qty': 0,
  65. }
  66. @api.model
  67. def _get_module_encoding(self, file_ext):
  68. return 'utf-8'
  69. # Overload Section
  70. @api.model
  71. def update_list(self):
  72. res = super().update_list()
  73. if self.env.context.get('analyse_installed_modules', False):
  74. self.search([('state', '=', 'installed')]).button_analyse_code()
  75. return res
  76. @api.multi
  77. def write(self, vals):
  78. res = super().write(vals)
  79. if vals.get('state', False) == 'installed':
  80. self.button_analyse_code()
  81. elif vals.get('state', False) == 'uninstalled'\
  82. and 'module_analysis' not in [x.name for x in self]:
  83. self.write(self._get_clean_analyse_values())
  84. return res
  85. # Public Section
  86. @api.multi
  87. def button_analyse_code(self):
  88. IrModuleAuthor = self.env['ir.module.author']
  89. IrModuleTypeRule = self.env['ir.module.type.rule']
  90. rules = IrModuleTypeRule.search([])
  91. cfg = self.env['ir.config_parameter']
  92. val = cfg.get_param('module_analysis.exclude_directories', '')
  93. exclude_directories = [x.strip() for x in val.split(',') if x.strip()]
  94. val = cfg.get_param('module_analysis.exclude_files', '')
  95. exclude_files = [x.strip() for x in val.split(',') if x.strip()]
  96. for module in self:
  97. _logger.info("Analysing Code for module %s ..." % (module.name))
  98. # Update Authors, based on manifest key
  99. authors = []
  100. if module.author and module.author[0] == '[':
  101. author_txt_list = safe_eval(module.author)
  102. else:
  103. author_txt_list = module.author.split(',')
  104. author_txt_list = [x.strip() for x in author_txt_list]
  105. author_txt_list = [x for x in author_txt_list if x]
  106. for author_txt in author_txt_list:
  107. authors.append(IrModuleAuthor._get_or_create(author_txt))
  108. author_ids = [x.id for x in authors]
  109. module.author_ids = author_ids
  110. # Update Module Type, based on rules
  111. module_type_id = rules._get_module_type_id_from_module(module)
  112. module.module_type_id = module_type_id
  113. # Get Path of module folder and parse the code
  114. module_path = get_module_path(module.name)
  115. # Get Files
  116. analysed_datas = self._get_analyse_data_dict()
  117. file_extensions = analysed_datas.keys()
  118. file_list = self._get_files_to_analyse(
  119. module_path, file_extensions, exclude_directories,
  120. exclude_files)
  121. for file_path, file_ext in file_list:
  122. file_res = pygount.source_analysis(
  123. file_path, '',
  124. encoding=self._get_module_encoding(file_ext))
  125. for k, v in analysed_datas.get(file_ext).items():
  126. v['value'] += getattr(file_res, k)
  127. # Update the module with the datas
  128. values = {}
  129. for file_ext, analyses in analysed_datas.items():
  130. for k, v in analyses.items():
  131. values[v['field']] = v['value']
  132. module.write(values)
  133. # Custom Section
  134. @api.model
  135. def _get_files_to_analyse(
  136. self, path, file_extensions, exclude_directories, exclude_files):
  137. res = []
  138. for root, dirs, files in os.walk(path, followlinks=True):
  139. if set(Path(root).parts) & set(exclude_directories):
  140. continue
  141. for name in files:
  142. if name in exclude_files:
  143. continue
  144. filename, file_extension = os.path.splitext(name)
  145. if file_extension in file_extensions:
  146. res.append((os.path.join(root, name), file_extension))
  147. return res
  148. @api.model
  149. def _get_analyse_data_dict(self):
  150. res_dict = self._get_analyse_settings().copy()
  151. for file_ext, analyse_dict in res_dict.items():
  152. for analyse_type, v in analyse_dict.items():
  153. analyse_dict[analyse_type] = {
  154. 'field': v,
  155. 'value': 0
  156. }
  157. return res_dict