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.

564 lines
19KB

  1. # Copyright (C) 2016-Today: Odoo Community Association (OCA)
  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 base64
  5. import logging
  6. import os
  7. from docutils.core import publish_string
  8. from odoo import _, api, fields, models, tools
  9. from odoo.tools import html_sanitize
  10. from odoo.tools.safe_eval import safe_eval
  11. from odoo.addons.base.models.ir_module import MyWriter
  12. _logger = logging.getLogger(__name__)
  13. class OdooModuleVersion(models.Model):
  14. _name = "odoo.module.version"
  15. _description = "Odoo Module Version"
  16. _order = "name, technical_name"
  17. _ICON_PATH = [
  18. "static/src/img/",
  19. "static/description/",
  20. ]
  21. _MANIFEST_KEYS_ALLOWED = ["data", "demo"]
  22. # Constant Section
  23. _SETTING_OVERRIDES = {
  24. "embed_stylesheet": False,
  25. "doctitle_xform": False,
  26. "output_encoding": "unicode",
  27. "xml_declaration": False,
  28. "file_insertion_enabled": False,
  29. "raw_enabled": False,
  30. }
  31. _ODOO_TYPE_SELECTION = [
  32. ("verticalization", "Vertical Solutions"),
  33. ("localization", "Localization"),
  34. ("connector", "Connector"),
  35. ("other", "Other"),
  36. ]
  37. _ODOO_DEVELOPMENT_STATUS_SELECTION = [
  38. ("alpha", "Alpha"),
  39. ("beta", "Beta"),
  40. ("production/stable", "Production/Stable"),
  41. ("mature", "Mature"),
  42. ]
  43. development_status = fields.Selection(
  44. string="Module maturity",
  45. selection=_ODOO_DEVELOPMENT_STATUS_SELECTION,
  46. readonly=True,
  47. )
  48. # Column Section
  49. name = fields.Char(string="Name", readonly=True, index=True)
  50. technical_name = fields.Char(
  51. string="Technical Name",
  52. readonly=True,
  53. index=True,
  54. help="Technical Name of the Module (Folder name).",
  55. )
  56. complete_name = fields.Char(
  57. string="Complete Name", compute="_compute_complete_name", store=True
  58. )
  59. auto_install = fields.Boolean(string="Auto Install", readonly=True)
  60. icon = fields.Char(string="Icon Path (Manifest)", readonly=True)
  61. module_id = fields.Many2one(
  62. comodel_name="odoo.module",
  63. string="Module",
  64. required=True,
  65. ondelete="cascade",
  66. index=True,
  67. auto_join=True,
  68. readonly=True,
  69. )
  70. repository_branch_id = fields.Many2one(
  71. comodel_name="github.repository.branch",
  72. string="Repository Branch",
  73. readonly=True,
  74. required=True,
  75. ondelete="cascade",
  76. )
  77. repository_id = fields.Many2one(
  78. comodel_name="github.repository",
  79. string="Repository",
  80. readonly=True,
  81. related="repository_branch_id.repository_id",
  82. store=True,
  83. )
  84. organization_serie_id = fields.Many2one(
  85. comodel_name="github.organization.serie",
  86. string="Organization Serie",
  87. readonly=True,
  88. store=True,
  89. compute="_compute_organization_serie_id",
  90. )
  91. license = fields.Char(string="License (Manifest)", readonly=True)
  92. license_id = fields.Many2one(
  93. comodel_name="odoo.license",
  94. string="License",
  95. readonly=True,
  96. compute="_compute_license_id",
  97. store=True,
  98. )
  99. summary = fields.Char(string="Summary (Manifest)", readonly=True)
  100. depends = fields.Char(string="Dependencies (Manifest)", readonly=True)
  101. dependency_module_ids = fields.Many2many(
  102. comodel_name="odoo.module",
  103. string="Dependencies",
  104. relation="module_version_dependency_rel",
  105. column1="module_version_id",
  106. column2="dependency_module_id",
  107. store=True,
  108. compute="_compute_dependency_module_ids",
  109. )
  110. website = fields.Char(string="Website (Manifest)", readonly=True)
  111. external_dependencies = fields.Char(
  112. string="External Dependencies (Manifest)", readonly=True
  113. )
  114. description_rst = fields.Char(string="RST Description (Manifest)", readonly=True)
  115. description_rst_html = fields.Html(
  116. string="HTML the RST Description",
  117. readonly=True,
  118. compute="_compute_description_rst_html",
  119. store=True,
  120. )
  121. version = fields.Char(string="Version (Manifest)", readonly=True)
  122. author = fields.Char(string="Author (Manifest)", readonly=True)
  123. author_ids = fields.Many2many(
  124. string="Authors",
  125. comodel_name="odoo.author",
  126. relation="github_module_version_author_rel",
  127. column1="module_version_id",
  128. column2="author_id",
  129. # multi="author",
  130. compute="_compute_author",
  131. store=True,
  132. )
  133. author_ids_description = fields.Char(
  134. string="Authors (Text)", compute="_compute_author",
  135. # multi="author",
  136. store=True
  137. )
  138. lib_python_ids = fields.Many2many(
  139. comodel_name="odoo.lib.python",
  140. string="Python Lib Dependencies",
  141. relation="module_version_lib_python_rel",
  142. column1="module_version_id",
  143. column2="lib_python_id",
  144. # multi="lib",
  145. compute="_compute_lib",
  146. store=True,
  147. )
  148. lib_python_ids_description = fields.Char(
  149. string="Python Lib Dependencies (Text)",
  150. compute="_compute_lib",
  151. # multi="lib",
  152. store=True,
  153. )
  154. lib_bin_ids = fields.Many2many(
  155. comodel_name="odoo.lib.bin",
  156. string="Bin Lib Dependencies",
  157. relation="module_version_lib_bin_rel",
  158. column1="module_version_id",
  159. column2="lib_bin_id",
  160. # multi="lib",
  161. compute="_compute_lib",
  162. store=True,
  163. )
  164. lib_bin_ids_description = fields.Char(
  165. string="Bin Lib Dependencies (Text)",
  166. compute="_compute_lib",
  167. # multi="lib",
  168. store=True,
  169. )
  170. odoo_type = fields.Selection(
  171. string="Odoo Type",
  172. selection=_ODOO_TYPE_SELECTION,
  173. store=True,
  174. compute="_compute_odoo_type",
  175. )
  176. image = fields.Binary(string="Icon Image", readonly=True, attachment=True)
  177. github_url = fields.Char(
  178. string="Github URL", compute="_compute_github_url", store=True, readonly=True
  179. )
  180. category_id = fields.Many2one(
  181. comodel_name="odoo.category", string="Category", readonly=True
  182. )
  183. full_module_path = fields.Char(string="Full Local Path to the module",)
  184. manifest_keys = fields.Char(string="Manifest keys (Manifest)", readonly=True)
  185. manifest_key_ids = fields.Many2many(
  186. comodel_name="odoo.manifest.key",
  187. string="Manifest keys",
  188. store=True,
  189. compute="_compute_manifest_key_ids",
  190. )
  191. analysis_rule_info_ids = fields.One2many(
  192. string="Analysis Rule Info ids",
  193. comodel_name="odoo.module.version.rule.info",
  194. inverse_name="module_version_id",
  195. # ondelete="cascade",
  196. readonly=True,
  197. )
  198. # Overload Section
  199. def unlink(self):
  200. # Analyzed repository branches should be reanalyzed
  201. if not self._context.get("dont_change_repository_branch_state", False):
  202. repository_branch_obj = self.env["github.repository.branch"]
  203. repository_branch_obj.search(
  204. [
  205. ("id", "in", self.mapped("repository_branch_id").ids),
  206. ("state", "=", "analyzed"),
  207. ]
  208. ).write({"state": "to_analyze"})
  209. return super(OdooModuleVersion, self).unlink()
  210. # Compute Section
  211. @api.depends(
  212. "repository_id.organization_id.github_login",
  213. "repository_id.name",
  214. "repository_branch_id.name",
  215. "repository_branch_id.local_path",
  216. "full_module_path",
  217. )
  218. def _compute_github_url(self):
  219. for version in self:
  220. repo_id = version.repository_id
  221. version.github_url = (
  222. "https://github.com/{organization_name}/"
  223. "{repository_name}/tree/{branch_name}/{rest_path}".format(
  224. organization_name=repo_id.organization_id.github_login,
  225. repository_name=repo_id.name,
  226. branch_name=version.repository_branch_id.name,
  227. rest_path=(version.full_module_path or "")[
  228. len(version.repository_branch_id.local_path or "") + 1 :
  229. ],
  230. )
  231. )
  232. @api.depends("repository_branch_id.repository_id.name")
  233. def _compute_odoo_type(self):
  234. for version in self:
  235. repository_name = version.repository_branch_id.repository_id.name
  236. if repository_name.startswith("l10n-"):
  237. version.odoo_type = "localization"
  238. elif repository_name.startswith("connector-"):
  239. version.odoo_type = "connector"
  240. elif repository_name.startswith("vertical-"):
  241. version.odoo_type = "verticalization"
  242. else:
  243. version.odoo_type = "other"
  244. @api.depends("technical_name", "repository_branch_id.complete_name")
  245. def _compute_complete_name(self):
  246. for version in self:
  247. version.complete_name = (
  248. version.repository_branch_id.complete_name
  249. + "/"
  250. + version.technical_name
  251. )
  252. @api.depends("description_rst")
  253. def _compute_description_rst_html(self):
  254. for version in self:
  255. if version.description_rst:
  256. try:
  257. output = publish_string(
  258. source=version.description_rst,
  259. settings_overrides=self._SETTING_OVERRIDES,
  260. writer=MyWriter(),
  261. )
  262. except Exception:
  263. output = (
  264. "<h1 style='color:red;'>"
  265. + _("Incorrect RST Description")
  266. + "</h1>"
  267. )
  268. else:
  269. output = html_sanitize(
  270. "<h1 style='color:gray;'>" + _("No Version Found") + "</h1>"
  271. )
  272. version.description_rst_html = html_sanitize(output)
  273. @api.depends("depends")
  274. def _compute_dependency_module_ids(self):
  275. module_obj = self.env["odoo.module"]
  276. for version in self:
  277. modules = []
  278. for module_name in version.depends.split(","):
  279. if module_name:
  280. # Weird case, some times 'depends' field is empty
  281. modules.append(module_obj.create_if_not_exist(module_name))
  282. version.dependency_module_ids = [x.id for x in modules]
  283. @api.depends("manifest_keys")
  284. def _compute_manifest_key_ids(self):
  285. manifest_key_obj = self.env["odoo.manifest.key"]
  286. for version in self:
  287. manifest_keys = []
  288. if version.manifest_keys:
  289. for manifest_key in version.manifest_keys.split(","):
  290. if manifest_key:
  291. manifest_keys.append(
  292. manifest_key_obj.create_if_not_exist(manifest_key)
  293. )
  294. version.manifest_key_ids = [x.id for x in manifest_keys]
  295. @api.depends("external_dependencies")
  296. def _compute_lib(self):
  297. lib_python_obj = self.env["odoo.lib.python"]
  298. lib_bin_obj = self.env["odoo.lib.bin"]
  299. for version in self:
  300. python_libs = []
  301. bin_libs = []
  302. my_eval = safe_eval(version.external_dependencies)
  303. for python_name in my_eval.get("python", []):
  304. python_libs.append(lib_python_obj.create_if_not_exist(python_name))
  305. for bin_name in my_eval.get("bin", []):
  306. bin_libs.append(lib_bin_obj.create_if_not_exist(bin_name))
  307. version.lib_python_ids = [x.id for x in python_libs]
  308. version.lib_python_ids_description = ", ".join(
  309. sorted([x.name for x in python_libs])
  310. )
  311. version.lib_bin_ids = [x.id for x in bin_libs]
  312. version.lib_bin_ids_description = ", ".join(
  313. sorted([x.name for x in bin_libs])
  314. )
  315. @api.depends("license")
  316. def _compute_license_id(self):
  317. license_obj = self.env["odoo.license"]
  318. for version in self:
  319. if version.license:
  320. version.license_id = license_obj.create_if_not_exist(version.license).id
  321. @api.depends("author")
  322. def _compute_author(self):
  323. odoo_author_obj = self.env["odoo.author"]
  324. for version in self:
  325. authors = []
  326. for item in [x.strip() for x in (version.author or "").split(",")]:
  327. if (
  328. item
  329. and item
  330. != version.repository_id.organization_id.default_author_text
  331. ):
  332. authors.append(odoo_author_obj.create_if_not_exist(item))
  333. authors = set(authors)
  334. version.author_ids = [x.id for x in authors]
  335. version.author_ids_description = ", ".join(
  336. sorted([x.name for x in authors])
  337. )
  338. @api.depends(
  339. "repository_branch_id",
  340. "repository_branch_id.organization_id",
  341. "repository_branch_id.organization_id.organization_serie_ids",
  342. "repository_branch_id.organization_id.organization_serie_ids.name",
  343. )
  344. def _compute_organization_serie_id(self):
  345. organization_serie_obj = self.env["github.organization.serie"]
  346. for module_version in self:
  347. res = organization_serie_obj.search(
  348. [
  349. (
  350. "organization_id",
  351. "=",
  352. module_version.repository_branch_id.organization_id.id,
  353. ),
  354. ("name", "=", module_version.repository_branch_id.name),
  355. ]
  356. )
  357. module_version.organization_serie_id = res and res[0].id or False
  358. @api.model
  359. def get_module_category(self, info):
  360. """Used to search the category of the module by the data given
  361. on the manifest.
  362. :param dict info: The parsed dictionary with the manifest data.
  363. :returns: recordset of the given category if exists.
  364. """
  365. category_obj = self.env["odoo.category"]
  366. category = info.get("category", False)
  367. other_categ = category_obj.search([("name", "=", "Other")], limit=1)
  368. if not category:
  369. return other_categ
  370. found_category = category_obj.search([("name", "=", category)], limit=1)
  371. return found_category or other_categ
  372. # Custom Section
  373. @api.model
  374. def manifest_2_odoo(self, info, repository_branch, module):
  375. author_list = (
  376. (type(info["author"]) == list)
  377. and info["author"]
  378. or (type(info["author"]) == tuple)
  379. and [x for x in info["author"]]
  380. or info["author"].split(",")
  381. )
  382. manifest_keys = []
  383. for manifest_key in self._MANIFEST_KEYS_ALLOWED:
  384. if manifest_key in info:
  385. manifest_keys.append(manifest_key)
  386. return {
  387. "name": info["name"],
  388. "technical_name": info["technical_name"],
  389. "summary": info["summary"],
  390. "website": info["website"],
  391. "version": info["version"],
  392. "license": info["license"],
  393. "auto_install": info["auto_install"],
  394. "icon": info["icon"],
  395. "description_rst": info["description"],
  396. "external_dependencies": info.get("external_dependencies", {}),
  397. "author": ",".join([x.strip() for x in sorted(author_list) if x.strip()]),
  398. "depends": ",".join([x for x in sorted(info["depends"]) if x]),
  399. "manifest_keys": ",".join([x for x in sorted(manifest_keys) if x]),
  400. "repository_branch_id": repository_branch.id,
  401. "module_id": module.id,
  402. "category_id": self.get_module_category(info).id or None,
  403. }
  404. @api.model
  405. def create_or_update_from_manifest(self, info, repository_branch, full_module_path):
  406. module_obj = self.env["odoo.module"]
  407. module_version = self.search(
  408. [
  409. ("technical_name", "=", str(info["technical_name"])),
  410. ("repository_branch_id", "=", repository_branch.id),
  411. ]
  412. )
  413. if not module_version:
  414. # Create new module version
  415. module = module_obj.create_if_not_exist(info["technical_name"])
  416. vals = self.manifest_2_odoo(info, repository_branch, module)
  417. vals["full_module_path"] = full_module_path
  418. module_version = self.create(vals)
  419. else:
  420. # Update module Version
  421. vals = self.manifest_2_odoo(
  422. info, repository_branch, module_version.module_id
  423. )
  424. vals["full_module_path"] = full_module_path
  425. module_version.write(vals)
  426. icon_path = False
  427. resize = False
  428. if info.get("images"):
  429. full_current_icon_path = os.path.join(
  430. full_module_path, info.get("images")[0]
  431. )
  432. if os.path.exists(full_current_icon_path):
  433. icon_path = full_current_icon_path
  434. else:
  435. for current_icon_path in self._ICON_PATH:
  436. full_current_icon_path = os.path.join(
  437. full_module_path, current_icon_path, "icon.png"
  438. )
  439. if os.path.exists(full_current_icon_path):
  440. icon_path = full_current_icon_path
  441. resize = True
  442. break
  443. if icon_path:
  444. image_enc = False
  445. try:
  446. with open(icon_path, "rb") as f:
  447. image = f.read()
  448. image_enc = base64.b64encode(image)
  449. if resize:
  450. image = tools.image.ImageProcess(image_enc, False)
  451. image.resize(96, 96)
  452. image_enc = image.image_base64(output_format="PNG")
  453. except Exception:
  454. _logger.warning("Unable to read or resize %s", icon_path)
  455. module_version.write({"image": image_enc})
  456. else:
  457. # Set the default icon
  458. try:
  459. with open(
  460. os.path.join(os.path.dirname(__file__), "../data/oca.png"), "rb"
  461. ) as f:
  462. image = base64.b64encode(f.read())
  463. module_version.write({"image": image})
  464. except Exception as e:
  465. _logger.error("Unable to read the OCA icon image, error is %s", e)
  466. @api.model
  467. def cron_clean_odoo_module_version(self):
  468. module_versions = self.search([])
  469. module_versions.clean_odoo_module_version()
  470. def clean_odoo_module_version(self):
  471. for module_version in self:
  472. # Compute path(s) to analyze
  473. paths = module_version.repository_branch_id._get_module_paths()
  474. found = False
  475. for path in paths:
  476. module_ver_path = os.path.join(path, module_version.technical_name)
  477. if os.path.exists(module_ver_path):
  478. found = True
  479. continue
  480. if not found:
  481. module_version._process_clean_module_version()
  482. return True
  483. def _process_clean_module_version(self):
  484. for module_version in self:
  485. module_version.unlink()
  486. return True
  487. class OdooModuleVersionRuleInfo(models.TransientModel):
  488. _inherit = "github.analysis.rule.info.mixin"
  489. _name = "odoo.module.version.rule.info"
  490. _description = "Odoo Module Vesion Rule Info"
  491. module_version_id = fields.Many2one(
  492. string="Module Version", comodel_name="odoo.module.version",
  493. )
  494. repository_branch_id = fields.Many2one(
  495. string="Repository Branch", comodel_name="github.repository.branch",
  496. )
上海开阖软件有限公司 沪ICP备12045867号-1