diff --git a/vcp_github/models/vcp_repository.py b/vcp_github/models/vcp_repository.py index 016e5e9..8f655d3 100644 --- a/vcp_github/models/vcp_repository.py +++ b/vcp_github/models/vcp_repository.py @@ -17,6 +17,12 @@ class VcpRepository(models.Model): _inherit = "vcp.repository" + def _get_repository_url(self): + result = super()._get_repository_url() + if not result and self.platform_id.host_id.type_id.code == "github": + return f"https://github.com/{self.platform_id.name}/{self.name}" + return result + def _update_branches_github(self): self.ensure_one() client = self.platform_id._get_github_clients()[0] diff --git a/vcp_github/models/vcp_user.py b/vcp_github/models/vcp_user.py index 7b85782..8f6d1ae 100644 --- a/vcp_github/models/vcp_user.py +++ b/vcp_github/models/vcp_user.py @@ -2,7 +2,15 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import models +import logging +from datetime import datetime + +import github3 + +from odoo import fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) class VcpUser(models.Model): @@ -13,3 +21,34 @@ def _get_contributor_url(self): if not result and self.host_id.type_id.code == "github": return f"https://github.com/{self.external_id}" return result + + def _prepare_user_vals(self, user): + return { + "name": user.name or user.login, + "email": user.email, + "avatar_url": user.avatar_url, + "company": user.company, + } + + def _update_information_github(self): + self.ensure_one() + # TODO maybe we should move the api key on the host ? + platform = self.env["vcp.platform"].search( + [("host_id", "=", self.host_id.id)], limit=1 + ) + client = platform._get_github_clients()[0] + try: + user = client.user(self.external_id) + self.write(self._prepare_user_vals(user)) + except github3.exceptions.ForbiddenError as e: + _logger.error(e) + rate = client.rate_limit() + reset = fields.Datetime.to_string( + datetime.utcfromtimestamp(rate["resources"]["core"]["reset"]) + ) + raise ValidationError(self.env._(f"Reset on {reset}")) from e + except github3.exceptions.NotFoundError: + _logger.warning( + "The user %s do not exist anymore, inactive it", self.external_id + ) + self.active = False diff --git a/vcp_management/data/ir_cron.xml b/vcp_management/data/ir_cron.xml index 7746dda..907d4a9 100644 --- a/vcp_management/data/ir_cron.xml +++ b/vcp_management/data/ir_cron.xml @@ -38,4 +38,13 @@ days False + + VCP: User Update + + code + model._cron_update_users(limit=100) + 1 + days + False + diff --git a/vcp_management/models/res_partner.py b/vcp_management/models/res_partner.py index 902e67b..3e3b7ba 100644 --- a/vcp_management/models/res_partner.py +++ b/vcp_management/models/res_partner.py @@ -2,7 +2,12 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 + +import requests + from odoo import api, fields, models +from odoo.exceptions import UserError class ResPartner(models.Model): @@ -32,6 +37,34 @@ class ResPartner(models.Model): "vcp.organization", inverse_name="partner_id", ) + image_1920 = fields.Image( + compute="_compute_image_1920", + store=True, + readonly=False, + ) + + @api.depends( + "vcp_user_ids.sync_image_to_partner", + "vcp_user_ids.avatar_url", + ) + def _compute_image_1920(self): + for record in self: + sync_user = record.vcp_user_ids.filtered("sync_image_to_partner") + if len(sync_user) > 1: + raise UserError( + self.env._( + "Only one Vcp User can be use for synchronising the main image" + ) + ) + elif sync_user.avatar_url: + try: + response = requests.get(sync_user.avatar_url, timeout=10) + response.raise_for_status() + except Exception as e: + raise UserError( + self.env._("Fail to download avatar, %s.please retry".format()) + ) from e + record.image_1920 = base64.b64encode(response.content).decode("utf-8") @api.depends() def _compute_vcp_contributions(self): diff --git a/vcp_management/models/vcp_repository_branch.py b/vcp_management/models/vcp_repository_branch.py index d769f82..23db0cb 100644 --- a/vcp_management/models/vcp_repository_branch.py +++ b/vcp_management/models/vcp_repository_branch.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import os import re +import traceback from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -11,6 +12,7 @@ class VcpRepositoryBranch(models.Model): _name = "vcp.repository.branch" _inherit = ["vcp.rule.information.mixin"] _description = "Links Branches with Repositories" + _order = "rule_failure_msg, repository_id, branch_id" branch_id = fields.Many2one( "vcp.branch", @@ -39,9 +41,14 @@ class VcpRepositoryBranch(models.Model): default=fields.Datetime.now, required=True, ) + rule_failure_msg = fields.Text() def _cron_process_branch_rules(self, limit): - branches = self.search([], limit=limit, order="update_rule_processing_date asc") + branches = self.search( + [], + limit=limit, + order="update_rule_processing_date asc", + ) for branch in branches: branch.process_rules() @@ -65,12 +72,25 @@ def _get_local_path(self): def process_rules(self): for record in self: rules = record._get_rules() - # This parameters dict can be used to store parameters that will - # be used by other rules. - parameters = {} - for rule in rules: - if re.match(rule.branch_pattern, record.branch_id.name): - rule._process_rule(record, parameters) + try: + with self.env.cr.savepoint(): + # This parameters dict can be used to store parameters that will + # be used by other rules. + parameters = {} + for rule in rules: + if re.match(rule.branch_pattern, record.branch_id.name): + rule._process_rule(record, parameters) + except Exception as e: + record.rule_failure_msg = ( + f"error: {e}\n\n traceback: {traceback.format_exc()}" + ) + # we need to purge the cache as _get_odoo_module keep the module name + # in cache and if a module have been creating during the try + # as this have been rollbacked we need to purge it from the cache + self.env.registry.clear_cache() + else: + record.rule_failure_msg = False + record.update_rule_processing_date = fields.Datetime.now() def _download_code(self): result = super()._download_code() diff --git a/vcp_management/models/vcp_user.py b/vcp_management/models/vcp_user.py index 1230e92..d7fbc8a 100644 --- a/vcp_management/models/vcp_user.py +++ b/vcp_management/models/vcp_user.py @@ -26,6 +26,14 @@ class VcpUser(models.Model): partner_id = fields.Many2one( "res.partner", ) + sync_image_to_partner = fields.Boolean( + help="Use the image user as image on the partner" + ) + user_update_date = fields.Datetime(readonly=True, default=fields.Datetime.now) + avatar_url = fields.Char(readonly=True) + email = fields.Char(readonly=True) + company = fields.Char(readonly=True) + active = fields.Boolean(readonly=True, default=True) _sql_constraints = [ ( @@ -40,3 +48,18 @@ def _get_contributor_url(self): def _get_contributors_name(self, kind, **kwargs): return self.partner_id.name or self.name + + def update_information(self): + self.ensure_one() + now = fields.Datetime.now() + getattr(self, f"_update_information_{self.host_id.type_id.code}")() + self.user_update_date = now + + def _cron_update_users(self, limit): + users = self.search( + [], + limit=limit, + order="user_update_date ASC", + ) + for user in users: + user.update_information() diff --git a/vcp_management/views/vcp_platform.xml b/vcp_management/views/vcp_platform.xml index 398622b..4a2707a 100644 --- a/vcp_management/views/vcp_platform.xml +++ b/vcp_management/views/vcp_platform.xml @@ -19,7 +19,7 @@ diff --git a/vcp_management/views/vcp_repository_branch.xml b/vcp_management/views/vcp_repository_branch.xml index 3bbd321..6100f47 100644 --- a/vcp_management/views/vcp_repository_branch.xml +++ b/vcp_management/views/vcp_repository_branch.xml @@ -6,6 +6,26 @@ vcp.repository.branch + + + + + + There is an exception blocking the rule processing: + + + @@ -29,6 +49,11 @@ vcp.repository.branch + @@ -43,6 +68,11 @@ + + diff --git a/vcp_management/views/vcp_user.xml b/vcp_management/views/vcp_user.xml index 8d328ad..e491f32 100644 --- a/vcp_management/views/vcp_user.xml +++ b/vcp_management/views/vcp_user.xml @@ -8,11 +8,22 @@ + + + + + - + + + + @@ -36,6 +47,7 @@ + diff --git a/vcp_odoo/__manifest__.py b/vcp_odoo/__manifest__.py index db9c82d..92983a2 100644 --- a/vcp_odoo/__manifest__.py +++ b/vcp_odoo/__manifest__.py @@ -15,8 +15,10 @@ "views/vcp_odoo_module_version.xml", "views/vcp_odoo_bin_package.xml", "views/vcp_odoo_python_library.xml", + "views/vcp_odoo_author.xml", "views/vcp_rule.xml", "views/menu.xml", + "views/res_partner.xml", "data/vcp_rule.xml", ], "demo": [], diff --git a/vcp_odoo/models/__init__.py b/vcp_odoo/models/__init__.py index dd294a2..d8ff12c 100644 --- a/vcp_odoo/models/__init__.py +++ b/vcp_odoo/models/__init__.py @@ -1,5 +1,7 @@ from . import vcp_rule from . import vcp_odoo_module +from . import vcp_odoo_author from . import vcp_odoo_module_version from . import vcp_odoo_bin_package from . import vcp_odoo_python_library +from . import res_partner diff --git a/vcp_odoo/models/res_partner.py b/vcp_odoo/models/res_partner.py new file mode 100644 index 0000000..8f47cc2 --- /dev/null +++ b/vcp_odoo/models/res_partner.py @@ -0,0 +1,42 @@ +# Copyright 2026 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + modules_maintained_count = fields.Integer( + compute="_compute_modules_maintained_count" + ) + + modules_author_count = fields.Integer(compute="_compute_modules_author_count") + + def _compute_modules_maintained_count(self): + for record in self: + record.modules_maintained_count = self.env["vcp.odoo.module"].search_count( + [("version_ids.maintainer_ids.partner_id", "=", record.id)], + ) + + def action_view_maintained_modules(self): + action = self.env["ir.actions.actions"]._for_xml_id( + "vcp_odoo.vcp_odoo_module_act_window" + ) + action["domain"] = [("version_ids.maintainer_ids.partner_id", "=", self.id)] + return action + + def _compute_modules_author_count(self): + for record in self: + record.modules_author_count = self.env["vcp.odoo.module"].search_count( + [("version_ids.author_ids.partner_id", "=", record.id)], + ) + + def action_view_author_modules(self): + action = self.env["ir.actions.actions"]._for_xml_id( + "vcp_odoo.vcp_odoo_module_act_window" + ) + action["domain"] = [("version_ids.author_ids.partner_id", "=", self.id)] + return action diff --git a/vcp_odoo/models/vcp_odoo_author.py b/vcp_odoo/models/vcp_odoo_author.py new file mode 100644 index 0000000..f1c9d01 --- /dev/null +++ b/vcp_odoo/models/vcp_odoo_author.py @@ -0,0 +1,38 @@ +# Copyright 2026 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models, tools + + +class VcpOdooAuthor(models.Model): + _name = "vcp.odoo.author" + _description = "Vcp Odoo Author" + + name = fields.Char() + partner_id = fields.Many2one("res.partner", "Partner") + + _sql_constraints = [("name_unique", "unique(name)", "Name must be uniq")] + + def _get_partner(self, name): + # Simple way to match the partner + partner = self.env["res.partner"].search( + [("name", "ilike", name), ("parent_id", "=", False)] + ) + if len(partner) > 1: + partner = partner.filtered(lambda s: s.name == name) + if len(partner) == 1: + return partner.id + else: + return None + + def _prepare_author(self, name): + return {"name": name, "partner_id": self._get_partner(name)} + + @tools.ormcache("name") + def _get_author(self, name): + author = self.search([("name", "=", name)], limit=1) + if not author: + vals = self._prepare_author(name) + author = self.create(vals) + return author.id diff --git a/vcp_odoo/models/vcp_odoo_module_version.py b/vcp_odoo/models/vcp_odoo_module_version.py index eb1b10f..a46b24a 100644 --- a/vcp_odoo/models/vcp_odoo_module_version.py +++ b/vcp_odoo/models/vcp_odoo_module_version.py @@ -35,6 +35,10 @@ class VcpOdooModuleVersion(models.Model): license = fields.Char(string="License (Manifest)", readonly=True) summary = fields.Char(string="Summary (Manifest)", readonly=True) website = fields.Char(string="Website (Manifest)", readonly=True) + development_status = fields.Char( + string="Development Status (Manifest)", + readonly=True, + ) python_library_ids = fields.Many2many( "vcp.odoo.python.library", string="Python Libraries", @@ -46,6 +50,16 @@ class VcpOdooModuleVersion(models.Model): readonly=True, ) description = fields.Html(readonly=True) + author_ids = fields.Many2many( + comodel_name="vcp.odoo.author", + string="Author", + readonly=True, + ) + maintainer_ids = fields.Many2many( + comodel_name="vcp.user", + string="Maintainer", + readonly=True, + ) def _get_local_path(self): return f"{self.repository_branch_id.local_path}/{self.path}" diff --git a/vcp_odoo/models/vcp_rule.py b/vcp_odoo/models/vcp_rule.py index 10e8aee..6dc0c78 100644 --- a/vcp_odoo/models/vcp_rule.py +++ b/vcp_odoo/models/vcp_rule.py @@ -106,6 +106,16 @@ def _process_rule_odoo_module_prepare_vals( package_bins.append( self.env["vcp.odoo.bin.package"]._get_bin_package(package_bin) ) + authors = [] + for author in manifest.get("author").split(","): + authors.append(self.env["vcp.odoo.author"]._get_author(author.strip())) + + maintainers = [] + for maintainer in manifest.get("maintainers", []): + maintainers.append( + repository_branch.platform_id.host_id._get_user(maintainer) + ) + description = False for html_description_path in self._get_html_description_path(): path = Path(os.path.dirname(manifest_path)) / html_description_path @@ -113,8 +123,10 @@ def _process_rule_odoo_module_prepare_vals( description = path.read_text() break return { - "name": manifest.get("name"), + "name": manifest.get("name").strip(), "module_id": module_id, + "author_ids": [Command.set(authors)], + "maintainer_ids": [Command.set(maintainers)], "version": manifest.get( "version", repository_branch.branch_id.name + ".0.0-dev" ), @@ -124,6 +136,7 @@ def _process_rule_odoo_module_prepare_vals( "license": manifest.get("license"), "summary": manifest.get("summary"), "website": manifest.get("website"), + "development_status": manifest.get("development_status"), "auto_install": manifest.get("auto_install", False), "repository_branch_id": repository_branch.id, "depends_on_module_ids": [Command.set(depends)], diff --git a/vcp_odoo/security/ir.model.access.csv b/vcp_odoo/security/ir.model.access.csv index 2fe5323..0ea2795 100644 --- a/vcp_odoo/security/ir.model.access.csv +++ b/vcp_odoo/security/ir.model.access.csv @@ -7,3 +7,5 @@ access_vcp_odoo_bin_package,Access Odoo Bin Package,model_vcp_odoo_bin_package,v manage_vcp_odoo_bin_package,Manage Odoo Bin Package,model_vcp_odoo_bin_package,vcp_management.group_vcp_manager,1,1,1,1 access_vcp_odoo_python_library,Access Odoo Python Library,model_vcp_odoo_python_library,vcp_management.group_vcp_user,1,0,0,0 manage_vcp_odoo_python_library,Manage Odoo Python Library,model_vcp_odoo_python_library,vcp_management.group_vcp_manager,1,1,1,1 +access_vcp_odoo_author,Access Odoo Author,model_vcp_odoo_author,vcp_management.group_vcp_user,1,0,0,0 +manage_vcp_odoo_author,Manage Odoo Author,model_vcp_odoo_author,vcp_management.group_vcp_manager,1,1,1,1 diff --git a/vcp_odoo/views/menu.xml b/vcp_odoo/views/menu.xml index befbdcf..c049831 100644 --- a/vcp_odoo/views/menu.xml +++ b/vcp_odoo/views/menu.xml @@ -21,8 +21,13 @@ action="vcp_odoo_module_version_act_window" sequence="20" /> + - + + + + res.partner + + + + + + + + + + + + + diff --git a/vcp_odoo/views/vcp_odoo_author.xml b/vcp_odoo/views/vcp_odoo_author.xml new file mode 100644 index 0000000..e4b5d8f --- /dev/null +++ b/vcp_odoo/views/vcp_odoo_author.xml @@ -0,0 +1,31 @@ + + + + vcp.odoo.author + + + + + + + + + + vcp.odoo.author + + + + + + + + + Author + ir.actions.act_window + vcp.odoo.author + list + + [] + {} + + diff --git a/vcp_odoo/views/vcp_odoo_module_version.xml b/vcp_odoo/views/vcp_odoo_module_version.xml index 3484034..d1bb9e8 100644 --- a/vcp_odoo/views/vcp_odoo_module_version.xml +++ b/vcp_odoo/views/vcp_odoo_module_version.xml @@ -8,6 +8,7 @@ + + + +
+ There is an exception blocking the rule processing: +