From 4c38c19eb88cddf25855419a0e88d8a863d9d01b Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 24 Jan 2026 10:41:21 +0100 Subject: [PATCH 1/7] [ADD] vcp --- vcp/README.rst | 91 ++++ vcp/__init__.py | 2 + vcp/__manifest__.py | 35 ++ vcp/controllers/__init__.py | 1 + vcp/controllers/main.py | 185 ++++++++ vcp/data/ir_cron.xml | 23 + vcp/models/__init__.py | 7 + vcp/models/res_partner.py | 68 +++ vcp/models/vcp_branch.py | 19 + vcp/models/vcp_comment.py | 36 ++ vcp/models/vcp_platform.py | 219 +++++++++ vcp/models/vcp_repository.py | 47 ++ vcp/models/vcp_request.py | 68 +++ vcp/models/vcp_review.py | 38 ++ vcp/pyproject.toml | 3 + vcp/readme/CONTEXT.md | 3 + vcp/readme/CONTRIBUTORS.md | 5 + vcp/readme/DESCRIPTION.md | 1 + vcp/security/ir.model.access.csv | 11 + vcp/static/description/icon.png | Bin 0 -> 2789 bytes vcp/static/description/icon.svg | 75 +++ vcp/static/description/index.html | 438 ++++++++++++++++++ .../popover_tooltip/popover_tooltip.esm.js | 8 + .../popover_tooltip/popover_tooltip.xml | 7 + .../components/vcp_render/vcp_render.esm.js | 122 +++++ .../src/components/vcp_render/vcp_render.scss | 31 ++ .../src/components/vcp_render/vcp_render.xml | 148 ++++++ vcp/static/tests/tours/portal.esm.js | 111 +++++ vcp/templates/templates.xml | 100 ++++ vcp/tests/__init__.py | 1 + vcp/tests/test_portal.py | 125 +++++ vcp/views/menu.xml | 11 + vcp/views/vcp_branch.xml | 27 ++ vcp/views/vcp_comment.xml | 26 ++ vcp/views/vcp_platform.xml | 83 ++++ vcp/views/vcp_repository.xml | 102 ++++ vcp/views/vcp_request.xml | 92 ++++ vcp/views/vcp_review.xml | 27 ++ 38 files changed, 2396 insertions(+) create mode 100644 vcp/README.rst create mode 100644 vcp/__init__.py create mode 100644 vcp/__manifest__.py create mode 100644 vcp/controllers/__init__.py create mode 100644 vcp/controllers/main.py create mode 100644 vcp/data/ir_cron.xml create mode 100644 vcp/models/__init__.py create mode 100644 vcp/models/res_partner.py create mode 100644 vcp/models/vcp_branch.py create mode 100644 vcp/models/vcp_comment.py create mode 100644 vcp/models/vcp_platform.py create mode 100644 vcp/models/vcp_repository.py create mode 100644 vcp/models/vcp_request.py create mode 100644 vcp/models/vcp_review.py create mode 100644 vcp/pyproject.toml create mode 100644 vcp/readme/CONTEXT.md create mode 100644 vcp/readme/CONTRIBUTORS.md create mode 100644 vcp/readme/DESCRIPTION.md create mode 100644 vcp/security/ir.model.access.csv create mode 100644 vcp/static/description/icon.png create mode 100644 vcp/static/description/icon.svg create mode 100644 vcp/static/description/index.html create mode 100644 vcp/static/src/components/popover_tooltip/popover_tooltip.esm.js create mode 100644 vcp/static/src/components/popover_tooltip/popover_tooltip.xml create mode 100644 vcp/static/src/components/vcp_render/vcp_render.esm.js create mode 100644 vcp/static/src/components/vcp_render/vcp_render.scss create mode 100644 vcp/static/src/components/vcp_render/vcp_render.xml create mode 100644 vcp/static/tests/tours/portal.esm.js create mode 100644 vcp/templates/templates.xml create mode 100644 vcp/tests/__init__.py create mode 100644 vcp/tests/test_portal.py create mode 100644 vcp/views/menu.xml create mode 100644 vcp/views/vcp_branch.xml create mode 100644 vcp/views/vcp_comment.xml create mode 100644 vcp/views/vcp_platform.xml create mode 100644 vcp/views/vcp_repository.xml create mode 100644 vcp/views/vcp_request.xml create mode 100644 vcp/views/vcp_review.xml diff --git a/vcp/README.rst b/vcp/README.rst new file mode 100644 index 0000000..8ebbbed --- /dev/null +++ b/vcp/README.rst @@ -0,0 +1,91 @@ +=== +Vcp +=== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e5814614bba4bc7f628d116d3529a253bb2ae9bff8e8a16c3cfc7eefa5def3cf + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fversion--control--platform-lightgray.png?logo=github + :target: https://github.com/OCA/version-control-platform/tree/18.0/vcp + :alt: OCA/version-control-platform +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/version-control-platform-18-0/version-control-platform-18-0-vcp + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/version-control-platform&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Creates a set of modules used for handling a version control patform. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +The aim of this module is to allow any community to import data from a +version control system. + +The system should be done in a way that is agnostic to the system and +the connections are handled directly by specific modules. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit + +Contributors +------------ + +- `Dixmit `__ + + - Enric Tobella + +- `Akretion `__ + + - Sebastien Beau + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/version-control-platform `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/vcp/__init__.py b/vcp/__init__.py new file mode 100644 index 0000000..91c5580 --- /dev/null +++ b/vcp/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/vcp/__manifest__.py b/vcp/__manifest__.py new file mode 100644 index 0000000..2f85b83 --- /dev/null +++ b/vcp/__manifest__.py @@ -0,0 +1,35 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Vcp", + "summary": """Virtual Control Platform core module""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/version-control-platform", + "depends": ["website_partner"], + "data": [ + "security/ir.model.access.csv", + "data/ir_cron.xml", + "templates/templates.xml", + "views/menu.xml", + "views/vcp_comment.xml", + "views/vcp_review.xml", + "views/vcp_request.xml", + "views/vcp_repository.xml", + "views/vcp_branch.xml", + "views/vcp_platform.xml", + ], + "demo": [], + "assets": { + "web.assets_frontend": [ + "vcp/static/src/components/**/*.esm.js", + "vcp/static/src/components/**/*.xml", + "vcp/static/src/components/**/*.scss", + ], + "web.assets_tests": [ + "vcp/static/tests/**/*", + ], + }, +} diff --git a/vcp/controllers/__init__.py b/vcp/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/vcp/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/vcp/controllers/main.py b/vcp/controllers/main.py new file mode 100644 index 0000000..f9b2080 --- /dev/null +++ b/vcp/controllers/main.py @@ -0,0 +1,185 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from math import sqrt + +from odoo import _, http +from odoo.http import request + +from odoo.addons.portal.controllers.portal import CustomerPortal + + +class ContributorsController(CustomerPortal): + @http.route( + [ + "/vcp", + "/vcp/", + ], + type="http", + auth="user", + website=True, + ) + def contributors_vcp(self, vcp=None): + values = self._prepare_portal_layout_values() + values.update(self._prepare_home_portal_values([])) + if vcp is None: + vcps = request.env["vcp.platform"].search([]) + return request.render( + "vcp.vcp_platforms_template", + {"vcps": vcps, **values}, + ) + vcp_id = ( + request.env["vcp.platform"] + .sudo() + .search([("name", "=ilike", vcp)], limit=1) + .id + ) + return request.render( + "vcp.vcp_platform_template", + {"vcp": vcp_id, **values}, + ) + + def _get_index(self, data): + return round( + sqrt(data["created_requests"]) + + data["merged_requests"] + + sqrt(data["comments"]) + + data["reviews"], + 2, + ) + + def _get_field(self, kind): + if kind == "contributors": + return "partner_id" + elif kind == "organizations": + return "organization_id" + elif kind == "repositories": + return "repository_id" + return False + + @http.route(["/vcp-fetch"], type="json", auth="user", readonly=True) + def fetch_vcp_data(self, vcp_id, year, month, kind, period, **values): + vcp = request.env["vcp.platform"].browse(vcp_id).exists() + if not vcp: + return [] + start, end = vcp._get_dates(year, month, period, **values) + data = vcp._generate_data(start, end, self._get_field(kind), kind, **values) + return { + "columns": self._get_vcp_columns(kind), + "data": self._improve_vcp_data(data, kind, **values), + } + + def _get_vcp_columns(self, kind): + if kind == "contributors": + return [ + {"field": "name", "title": _("Name"), "kind": "name"}, + { + "field": "created_requests", + "title": _("Created Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "merged_requests", + "title": _("Merged Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "comments", + "title": _("Comments"), + "kind": "float", + "decimals": 0, + }, + { + "field": "reviews", + "title": _("Reviews"), + "kind": "float", + "decimals": 0, + }, + ] + elif kind == "organizations": + return [ + {"field": "name", "title": _("Organization Name"), "kind": "name"}, + { + "field": "created_requests", + "title": _("Created Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "merged_requests", + "title": _("Merged Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "comments", + "title": _("Comments"), + "kind": "float", + "decimals": 0, + }, + { + "field": "reviews", + "title": _("Reviews"), + "kind": "float", + "decimals": 0, + }, + { + "field": "developers", + "title": _("Developers"), + "kind": "float", + "decimals": 0, + }, + ] + elif kind == "repositories": + return [ + {"field": "name", "title": _("Repository Name"), "kind": "name"}, + { + "field": "created_requests", + "title": _("Created Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "merged_requests", + "title": _("Merged Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "comments", + "title": _("Comments"), + "kind": "float", + "decimals": 0, + }, + { + "field": "reviews", + "title": _("Reviews"), + "kind": "float", + "decimals": 0, + }, + { + "field": "developers", + "title": _("Developers"), + "kind": "float", + "decimals": 0, + }, + ] + return [] + + def _improve_vcp_data(self, data, kind, **kwargs): + for key, values in data.items(): + if kind == "contributors": + partner = request.env["res.partner"].browse(key) + values["name"] = partner._get_contributors_name(kind, **kwargs) + values["url"] = partner._get_contributor_url() + elif kind == "organizations": + organization = request.env["res.partner"].browse(key) + values["name"] = organization._get_contributors_name(kind, **kwargs) + values["url"] = organization._get_contributor_url() + elif kind == "repositories": + repository = request.env["vcp.repository"].browse(key) + values["name"] = repository.name + values["url"] = repository._get_repository_url() + return data diff --git a/vcp/data/ir_cron.xml b/vcp/data/ir_cron.xml new file mode 100644 index 0000000..08a50e6 --- /dev/null +++ b/vcp/data/ir_cron.xml @@ -0,0 +1,23 @@ + + + + + VCP: Repository Update + + code + model._cron_update_repositories() + 1 + minutes + False + + + VCP: Platform Update + + code + model._cron_update_platforms() + 1 + days + False + + diff --git a/vcp/models/__init__.py b/vcp/models/__init__.py new file mode 100644 index 0000000..b0f8a8f --- /dev/null +++ b/vcp/models/__init__.py @@ -0,0 +1,7 @@ +from . import vcp_platform +from . import vcp_branch +from . import vcp_repository +from . import vcp_request +from . import vcp_review +from . import vcp_comment +from . import res_partner diff --git a/vcp/models/res_partner.py b/vcp/models/res_partner.py new file mode 100644 index 0000000..b2794bf --- /dev/null +++ b/vcp/models/res_partner.py @@ -0,0 +1,68 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + vcp_merged_requests = fields.Integer( + compute="_compute_vcp_contributions", + string="Merged Requests", + prefetch=False, + ) + vcp_created_requests = fields.Integer( + compute="_compute_vcp_contributions", + string="Created Requests", + prefetch=False, + ) + vcp_comments = fields.Integer( + compute="_compute_vcp_contributions", string="Comments", prefetch=False + ) + vcp_reviews = fields.Integer( + compute="_compute_vcp_contributions", string="Reviews", prefetch=False + ) + + @api.depends() + def _compute_vcp_contributions(self): + self.filtered(lambda p: p.github_user)._compute_vcp_contributions_field( + "partner_id" + ) + self.filtered(lambda p: not p.github_user)._compute_vcp_contributions_field( + "organization_id" + ) + + @api.model + def _get_contributors_field_map(self): + return { + "vcp_merged_requests": "merged_requests", + "vcp_created_requests": "created_requests", + "vcp_comments": "comments", + "vcp_reviews": "reviews", + } + + def _compute_vcp_contributions_field(self, field): + today = fields.Date.today() + start, end = self.env["vcp.platform"]._get_dates(today.year, today.month, "MAT") + data = ( + self.env["vcp.platform"] + .search([]) + ._generate_data( + start=start, end=end, field=field, kind="user", extra_domain=[] + ) + ) + field_map = self._get_contributors_field_map() + for partner in self: + partner.update( + {key: data[partner.id].get(field_map[key], 0) for key in field_map} + ) + + def _get_contributor_url(self): + if self.is_published and self.website_url: + return self.website_url + return False + + def _get_contributors_name(self, kind, **kwargs): + return self.name diff --git a/vcp/models/vcp_branch.py b/vcp/models/vcp_branch.py new file mode 100644 index 0000000..023803a --- /dev/null +++ b/vcp/models/vcp_branch.py @@ -0,0 +1,19 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class VcpBranch(models.Model): + _name = "vcp.branch" + _description = "Branch" + + name = fields.Char(required=True) + platform_id = fields.Many2one( + comodel_name="vcp.platform", + string="Platform", + required=True, + ) + _sql_constraints = [ + ("name_uniq", "unique(name, platform_id)", "Branch name must be unique.") + ] diff --git a/vcp/models/vcp_comment.py b/vcp/models/vcp_comment.py new file mode 100644 index 0000000..31ffcd7 --- /dev/null +++ b/vcp/models/vcp_comment.py @@ -0,0 +1,36 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class VcpComment(models.Model): + _name = "vcp.comment" + _description = "Comment" + + external_id = fields.Char(readonly=True, required=True, index=True) + body = fields.Html(readonly=True) + partner_id = fields.Many2one( + comodel_name="res.partner", + readonly=True, + ) + organization_id = fields.Many2one( + related="request_id.organization_id", + readonly=True, + store=True, + ) + repository_id = fields.Many2one( + related="request_id.repository_id", + readonly=True, + store=True, + ) + created_at = fields.Datetime(readonly=True) + updated_at = fields.Datetime(readonly=True) + request_id = fields.Many2one( + comodel_name="vcp.request", + string="Request", + readonly=True, + ) + _sql_constraints = [ + ("external_id_uniq", "unique(external_id)", "External ID must be unique.") + ] diff --git a/vcp/models/vcp_platform.py b/vcp/models/vcp_platform.py new file mode 100644 index 0000000..bb68359 --- /dev/null +++ b/vcp/models/vcp_platform.py @@ -0,0 +1,219 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging +from collections import defaultdict +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo import fields, models, tools + +_logger = logging.getLogger(__name__) + + +class VCPPlatform(models.Model): + """ + This model should define how to interact with a Version Control Platform + (VCP) such as GitHub, GitLab, etc. + 1 platform should correspond to 1 organization/account on the VCP. + """ + + _name = "vcp.platform" + _description = "VCP Platform" + + name = fields.Char(required=True) + description = fields.Char(readonly=True) + short_description = fields.Char(readonly=True) + last_update = fields.Datetime(readonly=True) + active = fields.Boolean(default=True) + update_interval_days = fields.Integer(default=3) + image_1920 = fields.Image() + branch_ids = fields.One2many( + "vcp.branch", + inverse_name="platform_id", + ) + image_128 = fields.Image( + max_width=128, + max_height=128, + store=True, + related="image_1920", + string="Image 128", + ) + image_64 = fields.Image( + max_width=64, max_height=64, store=True, related="image_1920", string="Image 64" + ) + kind = fields.Selection([]) + key_ids = fields.One2many( + comodel_name="vcp.platform.key", + inverse_name="platform_id", + string="API Keys", + ) + repository_ids = fields.One2many( + "vcp.repository", + inverse_name="platform_id", + ) + + def update_information(self): + self.ensure_one() + getattr(self, f"_update_information_{self.kind}")() + self.last_update = fields.Datetime.now() + + def _cron_update_platforms(self): + for organization in self.search([]): + try: + organization.update_information() + except Exception as e: + _logger.error( + "Error updating organization %s: %s", organization.name, str(e) + ) + + @tools.ormcache("self.id", "name") + def _get_branch(self, name): + self.ensure_one() + branch = self.env["vcp.branch"].search( + [("platform_id", "=", self.id), ("name", "=", name)], + limit=1, + ) + if not branch: + branch = ( + self.env["vcp.branch"] + .sudo() + .create( + { + "platform_id": self.id, + "name": name, + } + ) + ) + return branch.id + + def _get_merged_domain(self, start, end, **values): + return [ + ("repository_id.platform_id", "in", self.ids), + ("is_merged", "=", True), + ("closed_at", ">=", start), + ("closed_at", "<", end), + ] + + def _get_created_domain(self, start, end, **values): + return [ + ("repository_id.platform_id", "in", self.ids), + ("created_at", ">=", start), + ("created_at", "<", end), + ] + + def _get_comments_domain(self, start, end, **values): + return [ + ("request_id.repository_id.platform_id", "in", self.ids), + ("created_at", ">=", start), + ("created_at", "<", end), + ] + + def _get_reviews_domain(self, start, end, **values): + return [ + ("request_id.repository_id.platform_id", "in", self.ids), + ("submitted_at", ">=", start), + ("submitted_at", "<", end), + ] + + def _get_default_data(self, start, end, field, kind, **values): + return { + "name": "", + "github_name": "", + "created_requests": 0, + "merged_requests": 0, + "comments": 0, + "reviews": 0, + "developers": 0, + } + + def _generate_data(self, start, end, field, kind, extra_domain=None, **values): + if extra_domain is None: + extra_domain = [] + default_dict = self._get_default_data(start, end, field, kind, **values) + data = defaultdict(lambda: default_dict.copy()) + if not field: + return data + for merged in ( + self.env["vcp.request"] + .sudo() + .read_group( + self._get_merged_domain(start, end, **values) + + extra_domain + + [(field, "!=", False)], + [field], + [field], + ) + ): + data[merged[field][0]]["merged_requests"] = merged[f"{field}_count"] + for pr in ( + self.env["vcp.request"] + .sudo() + .read_group( + self._get_created_domain(start, end, **values) + + extra_domain + + [(field, "!=", False)], + [field, "partner_id:count_distinct"] + if field != "partner_id" + else [field], + [field], + ) + ): + data[pr[field][0]]["created_requests"] = pr[f"{field}_count"] + if field != "partner_id": + data[pr[field][0]]["developers"] = pr["partner_id"] + for comment in ( + self.env["vcp.comment"] + .sudo() + .read_group( + self._get_comments_domain(start, end, **values) + + extra_domain + + [(field, "!=", False)], + [field], + [field], + ) + ): + data[comment[field][0]]["comments"] = comment[f"{field}_count"] + for review in ( + self.env["vcp.review"] + .sudo() + .read_group( + self._get_reviews_domain(start, end, **values) + + extra_domain + + [(field, "!=", False)], + [field], + [field], + ) + ): + data[review[field][0]]["reviews"] = review[f"{field}_count"] + return data + + def _get_dates(self, year, month, period, **values): + if month == 12: + end = datetime(year + 1, 1, 1, 0, 0, 0) + else: + end = datetime(year, month + 1, 1, 0, 0, 0) + if period == "YTD": + start = datetime(year, 1, 1, 0, 0, 0) + elif period == "MAT": + start = end - relativedelta(years=1) + else: + start = datetime(year, month, 1, 0, 0, 0) + return start, end + + +class VcpPlatformKey(models.Model): + _name = "vcp.platform.key" + _description = "VCP Platform API Key" # TODO + + platform_id = fields.Many2one( + comodel_name="vcp.platform", + string="Platform", + required=True, + ondelete="cascade", + ) + name = fields.Char(required=True) + + _sql_constraints = [ + ("name_uniq", "unique(name, platform_id)", "API Key must be unique.") + ] diff --git a/vcp/models/vcp_repository.py b/vcp/models/vcp_repository.py new file mode 100644 index 0000000..7778659 --- /dev/null +++ b/vcp/models/vcp_repository.py @@ -0,0 +1,47 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class VcpRepository(models.Model): + _name = "vcp.repository" + _description = "Repository" + + name = fields.Char(required=True, index=True) + description = fields.Char(readonly=True) + platform_id = fields.Many2one( + comodel_name="vcp.platform", + required=True, + ) + created_at = fields.Datetime(readonly=True) + stargazers_count = fields.Integer(readonly=True) + fork_count = fields.Integer(readonly=True) + watchers_count = fields.Integer(readonly=True) + from_date = fields.Datetime(readonly=True, required=True) + request_ids = fields.One2many("vcp.request", inverse_name="repository_id") + request_count = fields.Integer(compute="_compute_request_count") + active = fields.Boolean(default=True) + + @api.depends("request_ids") + def _compute_request_count(self): + for record in self: + record.request_count = len(record.request_ids) + + def force_update_information(self): + self.update_information(update_interval_days=365) + + def update_information(self, update_interval_days=None): + self.ensure_one() + getattr(self, f"_update_information_{self.platform_id.kind}")( + update_interval_days=update_interval_days + ) + + def _cron_update_repositories(self, limit=1): + repositories = self.search([], limit=limit, order="from_date ASC") + for repository in repositories: + repository.update_information() + + def _get_repository_url(self): + self.ensure_one() + return False diff --git a/vcp/models/vcp_request.py b/vcp/models/vcp_request.py new file mode 100644 index 0000000..f5855b2 --- /dev/null +++ b/vcp/models/vcp_request.py @@ -0,0 +1,68 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models, tools + + +class VcpRequest(models.Model): + _name = "vcp.request" + _description = "Code Request" + + external_id = fields.Char(string="Externa ID", readonly=True, index=True) + name = fields.Char(readonly=True) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Contributor", + readonly=True, + ) + repository_id = fields.Many2one( + comodel_name="vcp.repository", + readonly=True, + ondelete="cascade", + ) + branch_id = fields.Many2one( + comodel_name="vcp.branch", + readonly=True, + ondelete="restrict", + ) + organization_id = fields.Many2one( + comodel_name="res.partner", + readonly=True, + ) + url = fields.Char(readonly=True) + state = fields.Char(readonly=True) + is_merged = fields.Boolean(readonly=True) + created_at = fields.Datetime(readonly=True) + updated_at = fields.Datetime(readonly=True) + closed_at = fields.Datetime(readonly=True) + number = fields.Integer(readonly=True) + label_ids = fields.Many2many( + comodel_name="vcp.request.label", + string="Labels", + readonly=True, + ) + commits = fields.Integer(readonly=True) + additions = fields.Integer(readonly=True) + deletions = fields.Integer(readonly=True) + total_comments = fields.Integer(readonly=True) + review_comments = fields.Integer(readonly=True) + + _sql_constraints = [ + ("external_id_uniq", "unique(external_id)", "External ID must be unique.") + ] + + +class VcpRequestLabel(models.Model): + _name = "vcp.request.label" + _description = "Vcp Request Label" + + name = fields.Char(required=True) + + _sql_constraints = [("name_uniq", "unique(name)", "Label name must be unique.")] + + @tools.ormcache("name") + def _get_label(self, name): + label = self.search([("name", "=", name)], limit=1) + if not label: + label = self.sudo().create({"name": name}) + return label.id diff --git a/vcp/models/vcp_review.py b/vcp/models/vcp_review.py new file mode 100644 index 0000000..c81ba05 --- /dev/null +++ b/vcp/models/vcp_review.py @@ -0,0 +1,38 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class VcpReview(models.Model): + _name = "vcp.review" + _description = "Review" # TODO + + external_id = fields.Char(readonly=True, required=True, index=True) + body = fields.Html(readonly=True) + state = fields.Char(readonly=True) + partner_id = fields.Many2one("res.partner", readonly=True) + submitted_at = fields.Datetime(readonly=True) + repository_id = fields.Many2one( + related="request_id.repository_id", + readonly=True, + store=True, + ) + request_id = fields.Many2one( + "vcp.request", + readonly=True, + ) + organization_id = fields.Many2one( + related="request_id.organization_id", + readonly=True, + store=True, + ) + platform_id = fields.Many2one( + related="request_id.repository_id.platform_id", + readonly=True, + store=True, + ) + + _sql_constraints = [ + ("external_id_uniq", "unique(external_id)", "External ID must be unique.") + ] diff --git a/vcp/pyproject.toml b/vcp/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/vcp/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/vcp/readme/CONTEXT.md b/vcp/readme/CONTEXT.md new file mode 100644 index 0000000..1c22f75 --- /dev/null +++ b/vcp/readme/CONTEXT.md @@ -0,0 +1,3 @@ +The aim of this module is to allow any community to import data from a version control system. + +The system should be done in a way that is agnostic to the system and the connections are handled directly by specific modules. diff --git a/vcp/readme/CONTRIBUTORS.md b/vcp/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..579d94d --- /dev/null +++ b/vcp/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- [Dixmit](https://dixmit.com) + - Enric Tobella + +- [Akretion](https://akretion.com) + - Sebastien Beau diff --git a/vcp/readme/DESCRIPTION.md b/vcp/readme/DESCRIPTION.md new file mode 100644 index 0000000..f05f973 --- /dev/null +++ b/vcp/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Creates a set of modules used for handling a version control patform. diff --git a/vcp/security/ir.model.access.csv b/vcp/security/ir.model.access.csv new file mode 100644 index 0000000..34d58cd --- /dev/null +++ b/vcp/security/ir.model.access.csv @@ -0,0 +1,11 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_vcp_platform,Access Platform,model_vcp_platform,base.group_user,1,0,0,0 +access_vcp_platform_portal,Access Platform from Portal,model_vcp_platform,base.group_portal,1,0,0,0 +manage_vcp_platform,Manage Platform,model_vcp_platform,base.group_system,1,1,1,0 +manage_vcp_platform_key,Manage Platform Keys,model_vcp_platform_key,base.group_system,1,1,1,1 +access_branch,Access Branch,model_vcp_branch,base.group_user,1,0,0,0 +access_repository,Access Repository,model_vcp_repository,base.group_user,1,0,0,0 +access_request,Access Pull Requests,model_vcp_request,base.group_user,1,0,0,0 +access_request_label,Access Pull Requests Labels,model_vcp_request_label,base.group_user,1,0,0,0 +access_review,Access Reviews,model_vcp_review,base.group_user,1,0,0,0 +access_comment,Access Comments,model_vcp_comment,base.group_user,1,0,0,0 diff --git a/vcp/static/description/icon.png b/vcp/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1fece979a0ac106449124efb87406041beee51da GIT binary patch literal 2789 zcmVpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H13UWzA zK~#90(uV z@%xvv)?WKt>z?er_Fn4{Gvm7q@of|MwxOC)%Dew)hCAphky1=&Nf|<+VZoxi^H0;; zCz@bFtR~TU28uPH3=9Vm!h&!l!;9AZ+u4_BI2rv5&~hjRQ~Pcp!u0?y1mKLJ_7xG{ zloBuWK3}jaJx`KD$9k$_!re@$128p)iU!7qAiO-{!|TUplP+X`cy z8>mDFb^*w?!X5z+%&zCBfs=2*tb2Oq60pV!<0ml*lGYQY{3bO`!=Yja%Z%fSa`>Ni=dl6Bwg`aDVshiKw4!-;(X_U+%;*xJ6Zlw;eyn3r@SRy% za%e^zb*VCDDU%o8dka7p8EH9yy*GP3d4;-G>HudefGd;vd-Jq#(Nw9mbg1^pn4lmK zVQ@)#^GqUEOb!tY%JPfM!UMKSo+1Z2vZN>1$h-u;6adv4+hgx_-r-z3b5<;Av8Gj* zhl8J)B$qU@TtoYbWJA3IIh$(y6TP$znMz-b9N-fki5~BNdW~ zkmVW*r8AyM1B*lnDU&M)K)lO&ZC@Aehw)1HR$ZD8@&&+mp79BT7L$A94i% z-AF1)m^v$$XyLOmS$>)EJ-d7q-#%}!M3Kame0m=wDN81YX| z+r|v7)7S@rVF94iNbXUT>LDTe%}g{|%7Q`}{v>&d#u)MCxY8Lfpmog-eNkg1C&o?z z=4L|+4Ex*8%tK};yvXaVCc@Dqc^cw`KYP9=ZT?io3dn(reVZU)yo*S}$Ev2PO{%W{ z5kSnn)+5B(U8=gPSktN#O=>4(E(io#K65z7b`B4KuTybg3nRe<3Dac;jA`Ec@L?!MaM`ryyVEydiO8!JT)R%D*`Z` zY_IqG zI0V8rNW4_Kl0Pz1iW`7P%((l|6rsy}5}Z>vvdezk;JPgU+$M%HNx8Iq>(&oU5(fYr z*huHI#QQLJK^u~cIw5eJC0N&SukrVBTp` zJy+=ZBZ-tGV-F`F)}G4nIKYHChhRJ{K_ zsjMVXmt;IW_a^R1j%945y#|b-wYD|`cKd#q&)Zis(9A^azUbq{$T|Lu$#Dk*jJXvn z*_$HOaj+}^0I>IpD^@VDAvp#Z%r5g1DSBAiLf!JTeyL9$SO9}+5Cipd>iIQG2*ct4 zfQVWzzkEBAC}!GoLm#;Eg6EGn!~J?*=fQB!p>v|tca+v+F<>2OYrDjl8ASJCv>PKK zNN}fXT5!=f&CpD7xa1sLSM^6Em9bY{f^eUe&I1$BCRed}^KJ%SNvsLDW(GZ=*q5;Y zAcC2TZ5<2S!6HbEmrGah=hiVjIfvB-0YD3f9|oYso!B8iu}ummEPIn~tFm|1d?Lem z)G~sRJSM`ZYO`I8_zf2Ucah()CK(N1`xFG;HT-*i-x5E_|Nj?nIJmbP=R=# zGdFauDPs)Hf6y5ns0EQJv=y*405TXX2D2eSXI^*^CYvyWhn+iEq9p3E+nJwlwtd$W zEDeCJ>v30hH78tP#tv&V82Cz^A9D_7%lZWYtJeS#zoSI3x%qs(?!5q=7E?%Mx@p4< zEDeC7>&N5CLnBJUW(zH#bZTB1%y)y#tm%3^fP@_-Ca@>$_g{@wt4!T8C5s9BaI&ho`Aey)7XbJ$ z4Kx4Hb-lKF+qU$Zh7B;GUyfEbHV(D9To(Yi55PzgLq`BSP*GD;Th-kB<;1WXh2<9_ zMrgg}nq1Pf70i4SfQlF@I)J?ZHiiR%7bfyokHr5h0Fn6g(o3^lMDvKKKmx;p+3#a+ rS=ih>A#L~Z@T~;k{|VpuugCujHAmnUkNyne00000NkvXXu0mjfjuIJ8 literal 0 HcmV?d00001 diff --git a/vcp/static/description/icon.svg b/vcp/static/description/icon.svg new file mode 100644 index 0000000..261d62c --- /dev/null +++ b/vcp/static/description/icon.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/vcp/static/description/index.html b/vcp/static/description/index.html new file mode 100644 index 0000000..19cb1b8 --- /dev/null +++ b/vcp/static/description/index.html @@ -0,0 +1,438 @@ + + + + + +Vcp + + + +
+

Vcp

+ + +

Beta License: AGPL-3 OCA/version-control-platform Translate me on Weblate Try me on Runboat

+

Creates a set of modules used for handling a version control patform.

+

Table of contents

+ +
+

Use Cases / Context

+

The aim of this module is to allow any community to import data from a +version control system.

+

The system should be done in a way that is agnostic to the system and +the connections are handled directly by specific modules.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/version-control-platform project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/vcp/static/src/components/popover_tooltip/popover_tooltip.esm.js b/vcp/static/src/components/popover_tooltip/popover_tooltip.esm.js new file mode 100644 index 0000000..0ad6447 --- /dev/null +++ b/vcp/static/src/components/popover_tooltip/popover_tooltip.esm.js @@ -0,0 +1,8 @@ +import {Component} from "@odoo/owl"; + +export class PopoverTooltip extends Component { + static template = "vcp.PopoverTooltip"; + static props = { + content: String, + }; +} diff --git a/vcp/static/src/components/popover_tooltip/popover_tooltip.xml b/vcp/static/src/components/popover_tooltip/popover_tooltip.xml new file mode 100644 index 0000000..268ffe9 --- /dev/null +++ b/vcp/static/src/components/popover_tooltip/popover_tooltip.xml @@ -0,0 +1,7 @@ + + +
+ +
+
+
diff --git a/vcp/static/src/components/vcp_render/vcp_render.esm.js b/vcp/static/src/components/vcp_render/vcp_render.esm.js new file mode 100644 index 0000000..2fe5c78 --- /dev/null +++ b/vcp/static/src/components/vcp_render/vcp_render.esm.js @@ -0,0 +1,122 @@ +import {Component, markup, onMounted, useState} from "@odoo/owl"; +import {Dropdown} from "@web/core/dropdown/dropdown"; +import {DropdownItem} from "@web/core/dropdown/dropdown_item"; +import {PopoverTooltip} from "../popover_tooltip/popover_tooltip.esm"; +import {formatFloat} from "@web/core/utils/numbers"; +import {registry} from "@web/core/registry"; +import {renderToString} from "@web/core/utils/render"; +import {rpc} from "@web/core/network/rpc"; +import {usePopover} from "@web/core/popover/popover_hook"; + +export class VcpRender extends Component { + static template = "vcp.VcpRender"; + setup() { + const year = new Date().getFullYear(); + const month = new Date().getMonth() + 1; + this.state = useState({ + sort: { + contributors: "index", + organizations: "merged_pull_requests", + repositories: "merged_pull_requests", + }, + columns: {}, + period: "YTD", + contributors: [], + organizations: [], + repositories: [], + year: month === 1 ? year - 1 : year, + month: (month === 1 ? 12 : month - 1).toString(), + kind: "contributors", + }); + onMounted(this.fetchData.bind(this)); + this.popover = usePopover(PopoverTooltip); + } + selectPeriod(period) { + this.state.period = period; + this.fetchData(); + } + initColumnTooltip(ev, column) { + this.popover.open(ev.currentTarget, { + content: markup(column.tooltip), + }); + } + initDateTooltip(ev) { + this.popover.open(ev.currentTarget, { + content: markup(renderToString("vcp.DateSelectionTooltip")), + }); + } + async fetchData() { + const data = await rpc("/vcp-fetch", this.getParameters()); + if (this.state.kind === "contributors") { + this.state.contributors = data.data; + } else if (this.state.kind === "organizations") { + this.state.organizations = data.data; + } else if (this.state.kind === "repositories") { + this.state.repositories = data.data; + } + this.state.columns = data.columns; + return data; + } + getParameters() { + return { + year: parseInt(this.state.year, 10), + month: parseInt(this.state.month, 10), + vcp_id: this.props.vcp, + kind: this.state.kind, + period: this.state.period, + }; + } + get rowIds() { + if (this.state.kind === "contributors") { + return Object.keys(this.state.contributors).sort( + (a, b) => + this.state.contributors[b][this.state.sort.contributors] - + this.state.contributors[a][this.state.sort.contributors] + ); + } else if (this.state.kind === "organizations") { + return Object.keys(this.state.organizations).sort( + (a, b) => + this.state.organizations[b][this.state.sort.organizations] - + this.state.organizations[a][this.state.sort.organizations] + ); + } else if (this.state.kind === "repositories") { + return Object.keys(this.state.repositories).sort( + (a, b) => + this.state.repositories[b][this.state.sort.repositories] - + this.state.repositories[a][this.state.sort.repositories] + ); + } + return []; + } + getRowData(row_id) { + if (this.state.kind === "organizations") { + return this.state.organizations[row_id]; + } + if (this.state.kind === "repositories") { + return this.state.repositories[row_id]; + } + if (this.state.kind === "contributors") { + return this.state.contributors[row_id]; + } + return {}; + } + formatFloat(value, digits) { + return formatFloat(value, {digits: [digits, digits]}); + } + setKind(kind) { + this.state.kind = kind; + this.fetchData(); + } + sortBy(field) { + this.state.sort[this.state.kind] = field; + } +} + +VcpRender.props = { + vcp: Number, +}; +VcpRender.components = { + Dropdown, + DropdownItem, +}; +registry.category("public_components").add("vcp.VcpRender", VcpRender); diff --git a/vcp/static/src/components/vcp_render/vcp_render.scss b/vcp/static/src/components/vcp_render/vcp_render.scss new file mode 100644 index 0000000..7e556a6 --- /dev/null +++ b/vcp/static/src/components/vcp_render/vcp_render.scss @@ -0,0 +1,31 @@ +.o_vcp_render { + .o_vcp_render_table { + .o_vcp_render_table_header { + &.sortable { + cursor: pointer; + } + } + } + .o_input { + input { + display: block; + width: 100%; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: white; + background-clip: padding-box; + border: var(--border-width) solid var(--border-color); + border-radius: var(--border-radius); + transition: + background-color 0.05s ease-in-out, + border-color 0.05s ease-in-out, + box-shadow 0.05s ease-in-out; + } + } +} diff --git a/vcp/static/src/components/vcp_render/vcp_render.xml b/vcp/static/src/components/vcp_render/vcp_render.xml new file mode 100644 index 0000000..2c10a84 --- /dev/null +++ b/vcp/static/src/components/vcp_render/vcp_render.xml @@ -0,0 +1,148 @@ + + +
+ Select the period for which you want to see the contributors statistics. + Options are: +
+
    +
  • MTD: Month-To-Date
  • +
  • YTD: Year-To-Date - From January to the selected month
  • +
  • MAT: Moving Annual Total - Last 12 Months
  • +
+
+ +
+

Contributors Statistics

+
+
+ +
+
+ +
+
+ + + + MTD + YTD + MAT + + +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + +
+
+
+ +
diff --git a/vcp/static/tests/tours/portal.esm.js b/vcp/static/tests/tours/portal.esm.js new file mode 100644 index 0000000..c30087f --- /dev/null +++ b/vcp/static/tests/tours/portal.esm.js @@ -0,0 +1,111 @@ +import {registry} from "@web/core/registry"; + +registry.category("web_tour.tours").add("portal_load_contributors_github", { + url: "/my", + steps: () => [ + { + content: "Check portal is loaded and Find Contributors menu", + trigger: 'a[href*="/vcp"]:contains("Virtual Control Platforms"):first', + run: "click", + expectUnloadPage: true, + }, + { + content: "Check Contributors Page", + trigger: 'a[href*="/vcp/oca"]:contains("OCA"):first', + run: "click", + expectUnloadPage: true, + }, + { + content: "Check Contributors Page", + trigger: "owl-component", + }, + { + content: "Check Etobella", + trigger: 'span:contains("Enric Tobella"):first', + }, + { + content: "Check LuisDixmit", + trigger: 'span:contains("Luis Rodriguez"):first', + }, + { + content: "Check JordiBForgeFlow", + trigger: 'span:contains("Jordi Ballester"):first', + }, + { + content: "Check Etobella Created Value", + trigger: 'tr:contains("Enric Tobella") td:nth-child(2):contains("1"):first', + }, + { + content: "Check LuisDixmit Created Value", + trigger: + 'tr:contains("Luis Rodriguez") td:nth-child(2):contains("1"):first', + }, + { + content: "Check JordiBForgeFlow Created Value", + trigger: + 'tr:contains("Jordi Ballester") td:nth-child(2):contains("1"):first', + }, + { + content: "Check Etobella Merged Value", + trigger: 'tr:contains("Enric Tobella") td:nth-child(3):contains("1"):first', + }, + { + content: "Check LuisDixmit Merged Value", + trigger: + 'tr:contains("Luis Rodriguez") td:nth-child(3):contains("0"):first', + }, + { + content: "Check JordiBForgeFlow Merged Value", + trigger: + 'tr:contains("Jordi Ballester") td:nth-child(3):contains("0"):first', + }, + { + content: "Change to Repositories", + trigger: ".o_vcp_repositories button", + run: "click", + }, + { + content: "Check Repository", + trigger: 'span:contains("contributors-module"):first', + }, + { + content: "Check Created Requests Value", + trigger: + 'tr:contains("contributors-module") td:nth-child(2):contains("3"):first', + }, + { + content: "Check Merged Requests Value", + trigger: + 'tr:contains("contributors-module") td:nth-child(3):contains("1"):first', + }, + { + content: "Change to Organizations", + trigger: ".o_vcp_organizations button", + run: "click", + }, + { + content: "Check Dixmit", + trigger: 'tr:contains("Dixmit"):first', + }, + { + content: "Check ForgeFlow", + trigger: 'tr:contains("ForgeFlow"):first', + }, + { + content: "Check Dixmit Created Pull Requests Value", + trigger: 'tr:contains("Dixmit") td:nth-child(2):contains("2"):first', + }, + { + content: "Check ForgeFlow Created Pull Requests Value", + trigger: 'tr:contains("ForgeFlow") td:nth-child(2):contains("1"):first', + }, + { + content: "Check Dixmit Merged Pull Requests Value", + trigger: 'tr:contains("Dixmit") td:nth-child(3):contains("1"):first', + }, + { + content: "Check ForgeFlow Merged Pull Requests Value", + trigger: 'tr:contains("ForgeFlow") td:nth-child(3):contains("0"):first', + }, + ], +}); diff --git a/vcp/templates/templates.xml b/vcp/templates/templates.xml new file mode 100644 index 0000000..f1ec432 --- /dev/null +++ b/vcp/templates/templates.xml @@ -0,0 +1,100 @@ + + + + + + + + diff --git a/vcp/tests/__init__.py b/vcp/tests/__init__.py new file mode 100644 index 0000000..8307da4 --- /dev/null +++ b/vcp/tests/__init__.py @@ -0,0 +1 @@ +from . import test_portal diff --git a/vcp/tests/test_portal.py b/vcp/tests/test_portal.py new file mode 100644 index 0000000..fc11101 --- /dev/null +++ b/vcp/tests/test_portal.py @@ -0,0 +1,125 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import timedelta + +from odoo.fields import Date +from odoo.tests import tagged + +from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal + + +@tagged("post_install", "-at_install") +class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal): + @classmethod + def setUpClass(cls): + super().setUpClass() + # be sure some expected values are set otherwise homepage may fail + date = Date.today() + date = date - timedelta(days=date.day) + cls.partner_portal.write( + { + "city": "Bayonne", + "company_name": "YourCompany", + "country_id": cls.env.ref("base.us").id, + "phone": "(683)-556-5104", + "street": "858 Lynn Street", + "zip": "07002", + } + ) + platform = cls.env["vcp.platform"].create( + { + "name": "oca", + "short_description": "OCA", + "description": "OCA", + } + ) + repository = cls.env["vcp.repository"].create( + { + "name": "contributors-module", + "description": "OCA/contributors-module", + "platform_id": platform.id, + "from_date": date, + } + ) + user_01 = cls.env["res.partner"].create( + { + "name": "Enric Tobella", + } + ) + user_02 = cls.env["res.partner"].create( + { + "name": "Luis Rodriguez", + } + ) + user_03 = cls.env["res.partner"].create( + { + "name": "Jordi Ballester", + } + ) + org_01 = cls.env["res.partner"].create( + { + "name": "Dixmit", + } + ) + org_02 = cls.env["res.partner"].create( + { + "name": "ForgeFlow", + } + ) + pull_request_01 = cls.env["vcp.request"].create( + { + "external_id": 1, + "name": "Test PR", + "repository_id": repository.id, + "partner_id": user_01.id, + "organization_id": org_01.id, + "created_at": date, + "closed_at": date, + "is_merged": True, + } + ) + cls.env["vcp.request"].create( + { + "external_id": 2, + "name": "Test PR", + "repository_id": repository.id, + "partner_id": user_02.id, + "organization_id": org_01.id, + "created_at": date, + "is_merged": False, + } + ) + cls.env["vcp.request"].create( + { + "external_id": 3, + "name": "Test PR", + "repository_id": repository.id, + "partner_id": user_03.id, + "organization_id": org_02.id, + "created_at": date, + "is_merged": False, + } + ) + cls.env["vcp.review"].create( + { + "external_id": 1, + "body": "Test Review", + "state": "APPROVED", + "request_id": pull_request_01.id, + "partner_id": user_01.id, + "submitted_at": date, + } + ) + cls.env["vcp.comment"].create( + { + "external_id": 1, + "body": "Test Comment", + "request_id": pull_request_01.id, + "partner_id": user_01.id, + "created_at": date, + } + ) + + def test_01_portal_load_tour(self): + self.start_tour("/", "portal_load_contributors_github", login="portal") diff --git a/vcp/views/menu.xml b/vcp/views/menu.xml new file mode 100644 index 0000000..ae210f4 --- /dev/null +++ b/vcp/views/menu.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/vcp/views/vcp_branch.xml b/vcp/views/vcp_branch.xml new file mode 100644 index 0000000..f5b6041 --- /dev/null +++ b/vcp/views/vcp_branch.xml @@ -0,0 +1,27 @@ + + + + + vcp.branch + +
+
+ + + + + + + + + + + vcp.branch + + + + + + + diff --git a/vcp/views/vcp_comment.xml b/vcp/views/vcp_comment.xml new file mode 100644 index 0000000..c43cec9 --- /dev/null +++ b/vcp/views/vcp_comment.xml @@ -0,0 +1,26 @@ + + + + + vcp.comment + +
+
+ + + + + + + + + + vcp.comment + + + + + + + diff --git a/vcp/views/vcp_platform.xml b/vcp/views/vcp_platform.xml new file mode 100644 index 0000000..39c5959 --- /dev/null +++ b/vcp/views/vcp_platform.xml @@ -0,0 +1,83 @@ + + + + + vcp.platform + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + vcp.platform + + + + + + + + + + vcp.platform + + + + + + + + + Platforms + vcp-platforms + vcp.platform + list,form + [] + {} + + + + Platforms + + + + + diff --git a/vcp/views/vcp_repository.xml b/vcp/views/vcp_repository.xml new file mode 100644 index 0000000..ca431aa --- /dev/null +++ b/vcp/views/vcp_repository.xml @@ -0,0 +1,102 @@ + + + + + vcp.repository + +
+
+
+ +
+ + + + +
+ + + + + +
+
+
+
+ + + vcp.repository + + + + + + + + + + vcp.repository + + + + + + + + + + + + Repositories + vcp-repositories + vcp.repository + list,form + [] + {} + + + + Repositories + + + + +
diff --git a/vcp/views/vcp_request.xml b/vcp/views/vcp_request.xml new file mode 100644 index 0000000..fd7b5ee --- /dev/null +++ b/vcp/views/vcp_request.xml @@ -0,0 +1,92 @@ + + + + + vcp.request + +
+
+ + + + + + + + + + + + + + + + + + + + + + + vcp.request + + + + + + + + + + + + + vcp.request + + + + + + + + + + + + + + Requests + vcp-requests + vcp.request + list,form + [] + {} + + + + Requests + vcp-platform-requests + vcp.request + list,form + [("repository_id", "=", active_id)] + {} + + + + Requests + + + + + diff --git a/vcp/views/vcp_review.xml b/vcp/views/vcp_review.xml new file mode 100644 index 0000000..6c20335 --- /dev/null +++ b/vcp/views/vcp_review.xml @@ -0,0 +1,27 @@ + + + + + vcp.review + +
+
+ + + + + + + + + + + vcp.review + + + + + + + From a3bf5228b9e65e3eb2b9ccea159201620e224e1e Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 24 Jan 2026 10:41:35 +0100 Subject: [PATCH 2/7] [ADD] vcp_github --- requirements.txt | 2 + vcp_github/README.rst | 211 +++++++++ vcp_github/__init__.py | 1 + vcp_github/__manifest__.py | 19 + vcp_github/models/__init__.py | 3 + vcp_github/models/res_partner.py | 123 +++++ vcp_github/models/vcp_platform.py | 79 ++++ vcp_github/models/vcp_repository.py | 193 ++++++++ vcp_github/pyproject.toml | 3 + vcp_github/readme/CONFIGURE.md | 10 + vcp_github/readme/CONTEXT.md | 16 + vcp_github/readme/CONTRIBUTORS.md | 2 + vcp_github/readme/CREDITS.md | 7 + vcp_github/readme/DESCRIPTION.md | 7 + vcp_github/readme/HISTORY.md | 22 + vcp_github/readme/INSTALL.md | 7 + vcp_github/readme/ROADMAP.md | 5 + vcp_github/readme/USAGE.md | 21 + vcp_github/static/description/icon.png | Bin 0 -> 9455 bytes vcp_github/static/description/index.html | 555 +++++++++++++++++++++++ vcp_github/tests/__init__.py | 1 + vcp_github/tests/test_github.py | 114 +++++ 22 files changed, 1401 insertions(+) create mode 100644 requirements.txt create mode 100644 vcp_github/README.rst create mode 100644 vcp_github/__init__.py create mode 100644 vcp_github/__manifest__.py create mode 100644 vcp_github/models/__init__.py create mode 100644 vcp_github/models/res_partner.py create mode 100644 vcp_github/models/vcp_platform.py create mode 100644 vcp_github/models/vcp_repository.py create mode 100644 vcp_github/pyproject.toml create mode 100644 vcp_github/readme/CONFIGURE.md create mode 100644 vcp_github/readme/CONTEXT.md create mode 100644 vcp_github/readme/CONTRIBUTORS.md create mode 100644 vcp_github/readme/CREDITS.md create mode 100644 vcp_github/readme/DESCRIPTION.md create mode 100644 vcp_github/readme/HISTORY.md create mode 100644 vcp_github/readme/INSTALL.md create mode 100644 vcp_github/readme/ROADMAP.md create mode 100644 vcp_github/readme/USAGE.md create mode 100644 vcp_github/static/description/icon.png create mode 100644 vcp_github/static/description/index.html create mode 100644 vcp_github/tests/__init__.py create mode 100644 vcp_github/tests/test_github.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f8976b3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +github3.py diff --git a/vcp_github/README.rst b/vcp_github/README.rst new file mode 100644 index 0000000..07627b8 --- /dev/null +++ b/vcp_github/README.rst @@ -0,0 +1,211 @@ +========== +Vcp Github +========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:086596db3dcb4fa7565fc97cf01ccd22449731243622d4587a1f609eab8bc121 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fversion--control--platform-lightgray.png?logo=github + :target: https://github.com/OCA/version-control-platform/tree/18.0/vcp_github + :alt: OCA/version-control-platform +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/version-control-platform-18-0/version-control-platform-18-0-vcp_github + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/version-control-platform&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +[ This file must be max 2-3 paragraphs, and is required. + +The goal of this document is to explain quickly the features of this +module: “what” this module does and “what” it is for. ] + +Example: + +This module extends the functionality of ... to support ... and to allow +users to ... + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +[ This file is optional but strongly suggested to allow end-users to +evaluate the module's usefulness in their context. ] + +BUSINESS NEED: It should explain the “why” of the module: + +- what is the business requirement that generated the need to develop + this module +- in which context or use cases this module can be useful (practical + examples are welcome!). + +APPROACH: It could also explain the approach to address the mentioned +need. + +USEFUL INFORMATION: It can also inform on related modules: + +- modules it depends on and their features +- other modules that can work well together with this one +- suggested setups where the module is useful (eg: multicompany, + multi-website) + +Installation +============ + +[ This file must only be present if there are very specific installation +instructions, such as installing non-python dependencies. The audience +is systems administrators. ] + +To install this module, you need to: + +1. Do this ... + +Configuration +============= + +[ This file is not always required; it should explain **how to configure +the module before using it**; it is aimed at users with administration +privileges. + +Please be detailed on the path to configuration (eg: do you need to +activate developer mode?), describe step by step configurations and the +use of screenshots is strongly recommended.] + +To configure this module, you need to: + +- Go to *App* > Menu > Menu item +- Activate boolean… > save +- … + +Usage +===== + +[ This file is required and contains the instructions on **“how”** to +use the module for end-users. + +If the module does not have a visible impact on the user interface, just +add the following sentence: + + This module does not impact the user interface. + +If that’s not the case, please make sure that every usage step is +covered and remember that images speak more than words!] + +To use this module, you need to: + +- Go to *App* > Menu > Menu item + + *insert screenshot!* + +- In “Contact” form, add a value to field *xyz* > save + + *insert screenshot!* + +- The value of *xyz* is now displayed in the list view. + + *insert screenshot!* + +Known issues / Roadmap +====================== + +[ Enumerate known caveats and future potential improvements. It is +mostly intended for end-users, and can also help potential new +contributors discovering new features to implement. ] + +- ... + +Changelog +========= + +[ The change log. The goal of this file is to help readers understand +changes between version. The primary audience is end users and +integrators. Purely technical changes such as code refactoring must not +be mentioned here. + +This file may contain ONE level of section titles, underlined with the ~ +(tilde) character. Other section markers are forbidden and will likely +break the structure of the README.rst or other documents where this +fragment is included. ] + +11.0.x.y.z (YYYY-MM-DD) +----------------------- + +- [BREAKING] Breaking changes come first. + (`#70 `__) +- [ADD] New feature. (`#74 `__) +- [FIX] Correct this. (`#71 `__) + +11.0.x.y.z (YYYY-MM-DD) +----------------------- + +- ... + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit + +Contributors +------------ + +- Firstname Lastname email.address@example.org (optional company website + url) +- Second Person second.person@example.org (optional company website url) + +Other credits +------------- + +[ This file is optional and contains additional credits, other than +authors, contributors, and maintainers. ] + +The development of this module has been financially supported by: + +- Company 1 name +- Company 2 name + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/version-control-platform `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/vcp_github/__init__.py b/vcp_github/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/vcp_github/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/vcp_github/__manifest__.py b/vcp_github/__manifest__.py new file mode 100644 index 0000000..9ddb87a --- /dev/null +++ b/vcp_github/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Vcp Github", + "summary": """Integrate Virtual Control Platform with Github""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/version-control-platform", + "depends": [ + "vcp", + ], + "external_dependencies": { + "python": ["github3.py"], + }, + "data": [], + "demo": [], +} diff --git a/vcp_github/models/__init__.py b/vcp_github/models/__init__.py new file mode 100644 index 0000000..8fe49b4 --- /dev/null +++ b/vcp_github/models/__init__.py @@ -0,0 +1,3 @@ +from . import vcp_platform +from . import vcp_repository +from . import res_partner diff --git a/vcp_github/models/res_partner.py b/vcp_github/models/res_partner.py new file mode 100644 index 0000000..f46fed1 --- /dev/null +++ b/vcp_github/models/res_partner.py @@ -0,0 +1,123 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import github3 + +from odoo import fields, models, tools + + +class ResPartner(models.Model): + _inherit = "res.partner" + + github_name = fields.Char( + string="GitHub Username", + help="GitHub username of the contributor or organization", + readonly=True, + ) + github_organization = fields.Boolean( + string="Is GitHub Organization", + help="Check if this partner represents a GitHub organization", + readonly=True, + ) + github_user = fields.Boolean( + string="Is GitHub User", + help="Check if this partner represents a GitHub user", + readonly=True, + ) + + _sql_constraints = [ + ( + "github_name_uniq", + "unique(github_name)", + "The GitHub username must be unique across partners.", + ), + ] + + @tools.ormcache("github_login") + def _get_github_user_id(self, github_login): + partner = self.with_context(active_test=False).search( + [("github_name", "=ilike", github_login)], limit=1 + ) + if not partner: + return False + if not partner.github_user: + partner.github_user = True + return partner.id + + @tools.ormcache("github_login") + def _get_github_organization_id(self, github_login): + partner = self.with_context(active_test=False).search( + [("github_name", "=ilike", github_login)], limit=1 + ) + if not partner: + return False + if not partner.github_organization: + partner.github_organization = True + return partner.id + + def _get_github_user(self, gh, client): + if not gh: + return False + if isinstance(gh, github3.users.User): + github_login = gh.login + else: + github_login = str(gh) + partner = self._get_github_user_id(github_login) + if partner: + return partner + if isinstance(gh, str): + try: + gh = client.user(github_login) + name = gh.name or github_login + except github3.exceptions.NotFoundError: + name = gh + else: + if hasattr(gh, "name"): + name = gh.name or github_login + else: + name = github_login + self.env.registry.clear_cache() + return self.create( + { + "name": name, + "github_name": github_login, + "github_user": True, + } + ).id + + def _get_github_organization(self, gh, client): + if not gh: + return False + partner = self._get_github_organization_id(str(gh)) + if partner: + return partner + self.env.registry.clear_cache() + try: + org = client.organization(str(gh)) + + return self.create( + { + "name": org.name or str(gh), + "github_name": str(gh), + "github_organization": True, + } + ).id + except github3.exceptions.NotFoundError: + user = client.user(str(gh)) + name = user.name or str(gh) + except github3.exceptions.ForbiddenError: + name = str(gh) + return self.create( + { + "name": name, + "github_name": str(gh), + "github_user": True, + "github_organization": True, + } + ).id + + def _get_contributor_url(self): + result = super()._get_contributor_url() + if not result and self.github_name: + return f"https://github.com/{self.github_name}" + return result diff --git a/vcp_github/models/vcp_platform.py b/vcp_github/models/vcp_platform.py new file mode 100644 index 0000000..656128f --- /dev/null +++ b/vcp_github/models/vcp_platform.py @@ -0,0 +1,79 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +from datetime import datetime + +import github3 +import requests +from pytz import UTC + +from odoo import fields, models + + +class VcpPlatform(models.Model): + _inherit = "vcp.platform" + + kind = fields.Selection( + selection_add=[("github", "GitHub")], + ondelete={"github": "cascade"}, + ) + + def _get_github_clients(self): + git = [] + for key in self.key_ids: + git.append(github3.login(token=key.name)) + return git + + def _update_information_github(self): + self.ensure_one() + clients = self._get_github_clients() + org = clients[0].organization(self.name) + self.short_description = org.name + self.description = org.description + if org.avatar_url: + response = requests.get(org.avatar_url, timeout=10) + response.raise_for_status() + self.image_1920 = base64.b64encode(response.content) + repos = org.repositories() + for repo in repos: + self._update_github_repository(repo) + self.last_update = fields.Datetime.now() + + def _parse_github_date(self, date): + if not date: + return False + return UTC.normalize( + datetime.fromisoformat(date.replace("Z", "+00:00")) + ).replace(tzinfo=None) + + def _update_github_repository(self, repo): + vals = { + "created_at": self._parse_github_date(repo.created_at), + "stargazers_count": repo.stargazers_count, + "fork_count": repo.forks_count, + "watchers_count": repo.watchers_count, + "description": repo.description, + } + repository = self.env["vcp.repository"].search( + [ + ("name", "=", repo.name), + ("platform_id", "=", self.id), + ], + limit=1, + ) + if not repository: + repository = ( + self.env["vcp.repository"] + .sudo() + .create( + { + "name": repo.name, + "platform_id": self.id, + "from_date": vals.get("created_at"), + **vals, + } + ) + ) + else: + repository.sudo().write(vals) diff --git a/vcp_github/models/vcp_repository.py b/vcp_github/models/vcp_repository.py new file mode 100644 index 0000000..6d1630a --- /dev/null +++ b/vcp_github/models/vcp_repository.py @@ -0,0 +1,193 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging +from datetime import datetime, timedelta + +import github3 +from github3 import pulls +from pytz import UTC + +from odoo import fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class VcpRepository(models.Model): + _inherit = "vcp.repository" + + def _parse_github_pr(self, pr, client): + origin_data = pr.as_dict() + comments_url = pr.comments_url + comments_req = client.session.get(comments_url) + comments = comments_req.json() + while comments_req.links.get("next"): + comments_url = comments_req.links["next"]["url"] + comments_req = client.session.get(comments_url) + comments += comments_req.json() + reviews_url = pr.reviews().url + reviews_req = client.session.get(reviews_url) + reviews = reviews_req.json() + while reviews_req.links.get("next"): + reviews_url = reviews_req.links["next"]["url"] + reviews_req = client.session.get(reviews_url) + reviews += reviews_req.json() + return ( + str(pr.id), + { + "partner_id": self.env["res.partner"]._get_github_user(pr.user, client), + "repository_id": self.id, + "branch_id": self.platform_id._get_branch(pr.base.ref), + "organization_id": self.env["res.partner"]._get_github_organization( + pr.head.repo[0], client + ), + "url": pr.html_url, + "state": pr.state, + "name": pr.title, + "is_merged": any(label["name"] == "merged 🎉" for label in pr.labels) + or pr.is_merged(), + "created_at": self.platform_id._parse_github_date( + origin_data["created_at"] + ), + "closed_at": self.platform_id._parse_github_date( + origin_data["closed_at"] + ), + "number": pr.number, + "updated_at": self.platform_id._parse_github_date( + origin_data["updated_at"] + ), + "label_ids": [fields.Command.clear()] + + [ + fields.Command.link( + self.env["vcp.request.label"]._get_label(label["name"]) + ) + for label in origin_data["labels"] + ], + "commits": origin_data["commits"], + "total_comments": origin_data["comments"], + "review_comments": origin_data["review_comments"], + "additions": origin_data["additions"], + "deletions": origin_data["deletions"], + }, + [ + { + "id": str(c["id"]), + "partner_id": c.get("user") + and self.env["res.partner"]._get_github_user( + c["user"].get("login"), client + ), + "body": c["body"], + "created_at": self.platform_id._parse_github_date(c["created_at"]), + "updated_at": self.platform_id._parse_github_date(c["updated_at"]), + } + for c in comments + ], + [ + { + "id": str(r["id"]), + "partner_id": r.get("user") + and self.env["res.partner"]._get_github_user( + r["user"].get("login"), client + ), + "body": r["body"], + "submitted_at": self.platform_id._parse_github_date( + r.get("submitted_at") + ), + "state": r["state"]["keyword"] + if isinstance(r["state"], dict) + else r["state"], + } + for r in reviews + ], + ) + + def _update_information_github( + self, update_interval_days=None, client_for_search=0 + ): + self.ensure_one() + clients = self.platform_id._get_github_clients() + try: + start = UTC.localize(self.from_date) + end = min( + start + + timedelta( + days=update_interval_days or self.platform_id.update_interval_days + ), + UTC.localize(datetime.now()), + ) + start += timedelta( + days=-1 + ) # Add buffer day to avoid missing PRs on boundary dates + i = client_for_search % len(clients) + for pr in clients[i].search_issues( + f"is:pr repo:{self.platform_id.name}/{self.name} " + f"updated:{start.isoformat()}..{end.isoformat()}" + ): + i = (1 + i) % len(clients) + pr_id, pr_data, comments, reviews = self._parse_github_pr( + clients[i]._instance_or_null( + pulls.PullRequest, + clients[i]._json( + pr.issue._get(pr.issue.pull_request_urls.get("url")), 200 + ), + ), + clients[i], + ) + opr = self.env["vcp.request"].search( + [("external_id", "=", pr_id), ("repository_id", "=", self.id)], + limit=1, + ) + if not opr: + opr = ( + self.env["vcp.request"] + .sudo() + .create({"external_id": pr_id, **pr_data}) + ) + else: + opr.sudo().write(pr_data) + for comment in comments: + comment_id = comment.pop("id") + ocomment = self.env["vcp.comment"].search( + [ + ("external_id", "=", comment_id), + ("repository_id", "=", self.id), + ], + limit=1, + ) + if not ocomment: + self.env["vcp.comment"].sudo().create( + { + "external_id": comment_id, + "request_id": opr.id, + **comment, + } + ) + else: + ocomment.sudo().write(comment) + for review in reviews: + review_id = review.pop("id") + oreview = self.env["vcp.review"].search( + [ + ("external_id", "=", review_id), + ("repository_id", "=", self.id), + ], + limit=1, + ) + if not oreview: + self.env["vcp.review"].sudo().create( + { + "external_id": review_id, + "request_id": opr.id, + **review, + } + ) + else: + oreview.sudo().write(review) + self.sudo().from_date = end.replace(tzinfo=None) + except github3.exceptions.ForbiddenError as e: + _logger.error(e) + rate = clients[i].rate_limit() + reset = fields.Datetime.to_string( + datetime.utcfromtimestamp(rate["resources"]["core"]["reset"]) + ) + raise ValidationError(self.env._(f"Reset on {reset}")) from e diff --git a/vcp_github/pyproject.toml b/vcp_github/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/vcp_github/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/vcp_github/readme/CONFIGURE.md b/vcp_github/readme/CONFIGURE.md new file mode 100644 index 0000000..2fdb0e6 --- /dev/null +++ b/vcp_github/readme/CONFIGURE.md @@ -0,0 +1,10 @@ +[ This file is not always required; it should explain **how to configure the module before using it**; it is aimed at users with administration privileges. + +Please be detailed on the path to configuration (eg: do you need to activate developer mode?), describe step by step configurations and the use of screenshots is strongly recommended.] + + +To configure this module, you need to: + +- Go to *App* > Menu > Menu item +- Activate boolean… > save +- … diff --git a/vcp_github/readme/CONTEXT.md b/vcp_github/readme/CONTEXT.md new file mode 100644 index 0000000..096235a --- /dev/null +++ b/vcp_github/readme/CONTEXT.md @@ -0,0 +1,16 @@ +[ This file is optional but strongly suggested to allow end-users to evaluate the +module's usefulness in their context. ] + +BUSINESS NEED: +It should explain the “why” of the module: +- what is the business requirement that generated the need to develop this module +- in which context or use cases this module can be useful (practical examples are welcome!). + +APPROACH: +It could also explain the approach to address the mentioned need. + +USEFUL INFORMATION: +It can also inform on related modules: +- modules it depends on and their features +- other modules that can work well together with this one +- suggested setups where the module is useful (eg: multicompany, multi-website) diff --git a/vcp_github/readme/CONTRIBUTORS.md b/vcp_github/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..7be72fb --- /dev/null +++ b/vcp_github/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Firstname Lastname (optional company website url) +- Second Person (optional company website url) diff --git a/vcp_github/readme/CREDITS.md b/vcp_github/readme/CREDITS.md new file mode 100644 index 0000000..9c2b025 --- /dev/null +++ b/vcp_github/readme/CREDITS.md @@ -0,0 +1,7 @@ +[ This file is optional and contains additional credits, other than + authors, contributors, and maintainers. ] + +The development of this module has been financially supported by: + +- Company 1 name +- Company 2 name diff --git a/vcp_github/readme/DESCRIPTION.md b/vcp_github/readme/DESCRIPTION.md new file mode 100644 index 0000000..2371a14 --- /dev/null +++ b/vcp_github/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +[ This file must be max 2-3 paragraphs, and is required. + +The goal of this document is to explain quickly the features of this module: “what” this module does and “what” it is for. ] + +Example: + +This module extends the functionality of ... to support ... and to allow users to ... diff --git a/vcp_github/readme/HISTORY.md b/vcp_github/readme/HISTORY.md new file mode 100644 index 0000000..a6daf58 --- /dev/null +++ b/vcp_github/readme/HISTORY.md @@ -0,0 +1,22 @@ +[ The change log. The goal of this file is to help readers + understand changes between version. The primary audience is + end users and integrators. Purely technical changes such as + code refactoring must not be mentioned here. + + This file may contain ONE level of section titles, underlined + with the ~ (tilde) character. Other section markers are + forbidden and will likely break the structure of the README.rst + or other documents where this fragment is included. ] + +## 11.0.x.y.z (YYYY-MM-DD) + +- [BREAKING] Breaking changes come first. + ([#70](https://github.com/OCA/repo/issues/70)) +- [ADD] New feature. + ([#74](https://github.com/OCA/repo/issues/74)) +- [FIX] Correct this. + ([#71](https://github.com/OCA/repo/issues/71)) + +## 11.0.x.y.z (YYYY-MM-DD) + +- ... diff --git a/vcp_github/readme/INSTALL.md b/vcp_github/readme/INSTALL.md new file mode 100644 index 0000000..77b98e7 --- /dev/null +++ b/vcp_github/readme/INSTALL.md @@ -0,0 +1,7 @@ +[ This file must only be present if there are very specific + installation instructions, such as installing non-python + dependencies. The audience is systems administrators. ] + +To install this module, you need to: + +1. Do this ... diff --git a/vcp_github/readme/ROADMAP.md b/vcp_github/readme/ROADMAP.md new file mode 100644 index 0000000..446840c --- /dev/null +++ b/vcp_github/readme/ROADMAP.md @@ -0,0 +1,5 @@ +[ Enumerate known caveats and future potential improvements. + It is mostly intended for end-users, and can also help + potential new contributors discovering new features to implement. ] + +- ... diff --git a/vcp_github/readme/USAGE.md b/vcp_github/readme/USAGE.md new file mode 100644 index 0000000..2cf1275 --- /dev/null +++ b/vcp_github/readme/USAGE.md @@ -0,0 +1,21 @@ +[ This file is required and contains the instructions on **“how”** to use the module for end-users. + +If the module does not have a visible impact on the user interface, just add the following sentence: + +> This module does not impact the user interface. + +If that’s not the case, please make sure that every usage step is covered and remember that images speak more than words!] + +To use this module, you need to: + +- Go to *App* > Menu > Menu item + + *insert screenshot!* + +- In “Contact” form, add a value to field *xyz* > save + + *insert screenshot!* + +- The value of *xyz* is now displayed in the list view. + + *insert screenshot!* diff --git a/vcp_github/static/description/icon.png b/vcp_github/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/vcp_github/static/description/index.html b/vcp_github/static/description/index.html new file mode 100644 index 0000000..63aa4a5 --- /dev/null +++ b/vcp_github/static/description/index.html @@ -0,0 +1,555 @@ + + + + + +Vcp Github + + + +
+

Vcp Github

+ + +

Beta License: AGPL-3 OCA/version-control-platform Translate me on Weblate Try me on Runboat

+

[ This file must be max 2-3 paragraphs, and is required.

+

The goal of this document is to explain quickly the features of this +module: “what” this module does and “what” it is for. ]

+

Example:

+

This module extends the functionality of … to support … and to allow +users to …

+

Table of contents

+ +
+

Use Cases / Context

+

[ This file is optional but strongly suggested to allow end-users to +evaluate the module’s usefulness in their context. ]

+

BUSINESS NEED: It should explain the “why” of the module:

+
    +
  • what is the business requirement that generated the need to develop +this module
  • +
  • in which context or use cases this module can be useful (practical +examples are welcome!).
  • +
+

APPROACH: It could also explain the approach to address the mentioned +need.

+

USEFUL INFORMATION: It can also inform on related modules:

+
    +
  • modules it depends on and their features
  • +
  • other modules that can work well together with this one
  • +
  • suggested setups where the module is useful (eg: multicompany, +multi-website)
  • +
+
+
+

Installation

+

[ This file must only be present if there are very specific installation +instructions, such as installing non-python dependencies. The audience +is systems administrators. ]

+

To install this module, you need to:

+
    +
  1. Do this …
  2. +
+
+
+

Configuration

+

[ This file is not always required; it should explain how to configure +the module before using it; it is aimed at users with administration +privileges.

+

Please be detailed on the path to configuration (eg: do you need to +activate developer mode?), describe step by step configurations and the +use of screenshots is strongly recommended.]

+

To configure this module, you need to:

+
    +
  • Go to App > Menu > Menu item
  • +
  • Activate boolean… > save
  • +
  • +
+
+
+

Usage

+

[ This file is required and contains the instructions on “how” to +use the module for end-users.

+

If the module does not have a visible impact on the user interface, just +add the following sentence:

+
+This module does not impact the user interface.
+

If that’s not the case, please make sure that every usage step is +covered and remember that images speak more than words!]

+

To use this module, you need to:

+
    +
  • Go to App > Menu > Menu item

    +

    insert screenshot!

    +
  • +
  • In “Contact” form, add a value to field xyz > save

    +

    insert screenshot!

    +
  • +
  • The value of xyz is now displayed in the list view.

    +

    insert screenshot!

    +
  • +
+
+
+

Known issues / Roadmap

+

[ Enumerate known caveats and future potential improvements. It is +mostly intended for end-users, and can also help potential new +contributors discovering new features to implement. ]

+
    +
  • +
+
+
+

Changelog

+

[ The change log. The goal of this file is to help readers understand +changes between version. The primary audience is end users and +integrators. Purely technical changes such as code refactoring must not +be mentioned here.

+

This file may contain ONE level of section titles, underlined with the ~ +(tilde) character. Other section markers are forbidden and will likely +break the structure of the README.rst or other documents where this +fragment is included. ]

+
+

11.0.x.y.z (YYYY-MM-DD)

+
    +
  • [BREAKING] Breaking changes come first. +(#70)
  • +
  • [ADD] New feature. (#74)
  • +
  • [FIX] Correct this. (#71)
  • +
+
+ +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

[ This file is optional and contains additional credits, other than +authors, contributors, and maintainers. ]

+

The development of this module has been financially supported by:

+
    +
  • Company 1 name
  • +
  • Company 2 name
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/version-control-platform project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/vcp_github/tests/__init__.py b/vcp_github/tests/__init__.py new file mode 100644 index 0000000..c63e32e --- /dev/null +++ b/vcp_github/tests/__init__.py @@ -0,0 +1 @@ +from . import test_github diff --git a/vcp_github/tests/test_github.py b/vcp_github/tests/test_github.py new file mode 100644 index 0000000..76cd448 --- /dev/null +++ b/vcp_github/tests/test_github.py @@ -0,0 +1,114 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +from unittest.mock import MagicMock, patch + +from odoo.fields import Command +from odoo.tests.common import TransactionCase + + +class TestGithub(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.platform = cls.env["vcp.platform"].create( + { + "name": "oca", + "kind": "github", + "key_ids": [ + Command.create({"name": "ghp_exampletoken1234567890abcdef"}) + ], + } + ) + + def test_update_organization_and_logo(self): + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + with ( + patch("odoo.addons.vcp_github.models.vcp_platform.github3") as mock_github3, + patch( + "odoo.addons.vcp_github.models.vcp_platform.requests.get" + ) as mock_requests_get, + ): + mock_client = MagicMock() + mock_org = MagicMock() + mock_org.name = "Odoo Community Association" + mock_org.avatar_url = f"{base_url}/logo.png" + mock_requests_get.return_value.content = base64.b64decode( + self.env.company.logo + ) + mock_client.organization.return_value = mock_org + mock_github3.login.return_value = mock_client + self.platform.update_information() + self.assertEqual( + self.platform.short_description, "Odoo Community Association" + ) + mock_github3.login.assert_called_once_with( + token="ghp_exampletoken1234567890abcdef" + ) + mock_client.organization.assert_called_once_with("oca") + + def test_update_organization_with_repository(self): + with patch( + "odoo.addons.vcp_github.models.vcp_platform.github3" + ) as mock_github3: + mock_client = MagicMock() + mock_org = MagicMock() + mock_org.name = "Odoo Community Association" + mock_org.avatar_url = False + mock_repo1 = MagicMock() + mock_repo1.name = "server-tools" + mock_repo1.created_at = "2020-01-01T00:00:00Z" + mock_repo2 = MagicMock() + mock_repo2.name = "server-brand" + mock_repo2.created_at = "2021-01-01T10:00:00Z" + mock_org.repositories.return_value = [mock_repo1, mock_repo2] + mock_client.organization.return_value = mock_org + mock_github3.login.return_value = mock_client + self.platform.update_information() + self.assertEqual(len(self.platform.repository_ids), 2) + repo_names = {repo.name for repo in self.platform.repository_ids} + self.assertSetEqual(repo_names, {"server-tools", "server-brand"}) + mock_github3.login.assert_called_once_with( + token="ghp_exampletoken1234567890abcdef" + ) + mock_client.organization.assert_called_once_with("oca") + return self.platform.repository_ids.filtered(lambda r: r.name == "server-tools") + + def test_update_repository(self): + repository = self.test_update_organization_with_repository() + self.assertFalse(repository.request_ids) + with patch( + "odoo.addons.vcp_github.models.vcp_platform.github3.login" + ) as mock_client: + mock_login = MagicMock() + mock_client.return_value = mock_login + mock_login.session.get.return_value = MagicMock( + links={}, + json=lambda: [], + ) + mock_issue_request = MagicMock( + as_dict=lambda: { + "id": 1, + "user": {"login": "contributor1"}, + "base": {"ref": "main"}, + "head": {"repo": [MagicMock()]}, + "html_url": "https://github.com/oca/server-tools/pull/1", + "state": "closed", + "title": "Fix issue", + "labels": [{"name": "merged 🎉"}], + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-03T00:00:00Z", + "closed_at": "2023-01-02T00:00:00Z", + "commits": 3, + "comments": 2, + "review_comments": 1, + "additions": 150, + "deletions": 50, + "number": 1, + } + ) + mock_login._instance_or_null.return_value = mock_issue_request + mock_login.search_issues.return_value = [mock_issue_request] + repository.update_information() + self.assertTrue(repository.request_ids) From 2970f890c116575ee5a03e758d1e9b9aa1ffde9f Mon Sep 17 00:00:00 2001 From: Sylvain LE GAL Date: Sun, 25 Jan 2026 00:10:39 +0100 Subject: [PATCH 3/7] [FIX] vcp: set kind field required at platform level. It doesn't makes sense to have a plateform without kind + it makes failing call of update_information AttributeError: 'vcp.platform' object has no attribute '_update_information_False'. Did you mean: '_update_information_github'? --- test-requirements.txt | 1 + vcp/models/vcp_platform.py | 2 +- vcp/tests/models/__init__.py | 1 + vcp/tests/models/vcp_platform.py | 16 ++++++++++++++++ vcp/tests/test_portal.py | 10 ++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 test-requirements.txt create mode 100644 vcp/tests/models/__init__.py create mode 100644 vcp/tests/models/vcp_platform.py diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..66bc2cb --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo_test_helper diff --git a/vcp/models/vcp_platform.py b/vcp/models/vcp_platform.py index bb68359..8d6e3d7 100644 --- a/vcp/models/vcp_platform.py +++ b/vcp/models/vcp_platform.py @@ -42,7 +42,7 @@ class VCPPlatform(models.Model): image_64 = fields.Image( max_width=64, max_height=64, store=True, related="image_1920", string="Image 64" ) - kind = fields.Selection([]) + kind = fields.Selection([], required=True) key_ids = fields.One2many( comodel_name="vcp.platform.key", inverse_name="platform_id", diff --git a/vcp/tests/models/__init__.py b/vcp/tests/models/__init__.py new file mode 100644 index 0000000..ccba792 --- /dev/null +++ b/vcp/tests/models/__init__.py @@ -0,0 +1 @@ +from . import vcp_platform diff --git a/vcp/tests/models/vcp_platform.py b/vcp/tests/models/vcp_platform.py new file mode 100644 index 0000000..c37936e --- /dev/null +++ b/vcp/tests/models/vcp_platform.py @@ -0,0 +1,16 @@ +# Copyright 2026 GRAP +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class VCPPlatform(models.Model): + _inherit = "vcp.platform" + + kind = fields.Selection( + selection_add=[("dummy", "Dummy Value")], + ondelete={"dummy": "cascade"}, + ) diff --git a/vcp/tests/test_portal.py b/vcp/tests/test_portal.py index fc11101..d47ea73 100644 --- a/vcp/tests/test_portal.py +++ b/vcp/tests/test_portal.py @@ -3,6 +3,8 @@ from datetime import timedelta +from odoo_test_helper import FakeModelLoader + from odoo.fields import Date from odoo.tests import tagged @@ -14,6 +16,13 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal): @classmethod def setUpClass(cls): super().setUpClass() + # Load fake order model + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .models.vcp_platform import VCPPlatform + + cls.loader.update_registry((VCPPlatform,)) + # be sure some expected values are set otherwise homepage may fail date = Date.today() date = date - timedelta(days=date.day) @@ -32,6 +41,7 @@ def setUpClass(cls): "name": "oca", "short_description": "OCA", "description": "OCA", + "kind": "dummy", } ) repository = cls.env["vcp.repository"].create( From 3a4a3a08459f6df37a6e8154f2b97287e22a5b6e Mon Sep 17 00:00:00 2001 From: Sylvain LE GAL Date: Sun, 25 Jan 2026 00:14:56 +0100 Subject: [PATCH 4/7] [FIX] vcp: handle correctly the error, if no API Keys are defined --- vcp_github/models/vcp_platform.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vcp_github/models/vcp_platform.py b/vcp_github/models/vcp_platform.py index 656128f..ddd91c7 100644 --- a/vcp_github/models/vcp_platform.py +++ b/vcp_github/models/vcp_platform.py @@ -8,7 +8,8 @@ import requests from pytz import UTC -from odoo import fields, models +from odoo import _, fields, models +from odoo.exceptions import ValidationError class VcpPlatform(models.Model): @@ -28,6 +29,10 @@ def _get_github_clients(self): def _update_information_github(self): self.ensure_one() clients = self._get_github_clients() + if not clients: + raise ValidationError( + _("No github clients configured. Please enter at least an API Key.") + ) org = clients[0].organization(self.name) self.short_description = org.name self.description = org.description From 7dbbda57ac65613634f8b9c10e73b3f18bf42087 Mon Sep 17 00:00:00 2001 From: Sylvain LE GAL Date: Sun, 25 Jan 2026 01:25:14 +0100 Subject: [PATCH 5/7] [REF] vcp: split into vcp and website_partner to make the module more modular. Rational: vcp contains a lot of thing that are interesting to use, even if website is not installed [ADD] website_partner description and screenshot --- vcp/__manifest__.py | 2 +- vcp/templates/templates.xml | 33 -- vcp_website_partner/README.rst | 80 ++++ vcp_website_partner/__init__.py | 0 vcp_website_partner/__manifest__.py | 17 + vcp_website_partner/i18n/fr.po | 35 ++ vcp_website_partner/pyproject.toml | 3 + vcp_website_partner/readme/DESCRIPTION.md | 6 + .../static/description/index.html | 421 ++++++++++++++++++ .../description/website_partner_form.png | Bin 0 -> 45139 bytes vcp_website_partner/templates/templates.xml | 36 ++ 11 files changed, 599 insertions(+), 34 deletions(-) create mode 100644 vcp_website_partner/README.rst create mode 100644 vcp_website_partner/__init__.py create mode 100644 vcp_website_partner/__manifest__.py create mode 100644 vcp_website_partner/i18n/fr.po create mode 100644 vcp_website_partner/pyproject.toml create mode 100644 vcp_website_partner/readme/DESCRIPTION.md create mode 100644 vcp_website_partner/static/description/index.html create mode 100644 vcp_website_partner/static/description/website_partner_form.png create mode 100644 vcp_website_partner/templates/templates.xml diff --git a/vcp/__manifest__.py b/vcp/__manifest__.py index 2f85b83..f0711c4 100644 --- a/vcp/__manifest__.py +++ b/vcp/__manifest__.py @@ -8,7 +8,7 @@ "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/version-control-platform", - "depends": ["website_partner"], + "depends": ["website"], "data": [ "security/ir.model.access.csv", "data/ir_cron.xml", diff --git a/vcp/templates/templates.xml b/vcp/templates/templates.xml index f1ec432..a02a163 100644 --- a/vcp/templates/templates.xml +++ b/vcp/templates/templates.xml @@ -64,37 +64,4 @@
-
diff --git a/vcp_website_partner/README.rst b/vcp_website_partner/README.rst new file mode 100644 index 0000000..a1689d7 --- /dev/null +++ b/vcp_website_partner/README.rst @@ -0,0 +1,80 @@ +===================== +Vcp - Website Partner +===================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0471d7839f1265a582540c4f4e8dd6b37fb02dcc94fc7ae9c1bf9474f8466efc + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fversion--control--platform-lightgray.png?logo=github + :target: https://github.com/OCA/version-control-platform/tree/18.0/vcp_website_partner + :alt: OCA/version-control-platform +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/version-control-platform-18-0/version-control-platform-18-0-vcp_website_partner + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/version-control-platform&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of ``website_partner`` module, +when ``vcp`` module is installed. + +It adds on partner website form view, some indicators regarding +contributions. + +|website_partner_form| + +.. |website_partner_form| image:: https://raw.githubusercontent.com/OCA/version-control-platform/18.0/vcp_website_partner/static/description/website_partner_form.png + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit +* GRAP + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/version-control-platform `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/vcp_website_partner/__init__.py b/vcp_website_partner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vcp_website_partner/__manifest__.py b/vcp_website_partner/__manifest__.py new file mode 100644 index 0000000..3726118 --- /dev/null +++ b/vcp_website_partner/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2026 GRAP +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Vcp - Website Partner", + "summary": "Glue module between Virtual Control Platform and Website Partner", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,GRAP,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/version-control-platform", + "depends": ["vcp", "website_partner"], + "data": [ + "templates/templates.xml", + ], + "demo": [], + "auto_install": True, +} diff --git a/vcp_website_partner/i18n/fr.po b/vcp_website_partner/i18n/fr.po new file mode 100644 index 0000000..74ed903 --- /dev/null +++ b/vcp_website_partner/i18n/fr.po @@ -0,0 +1,35 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * vcp_website_partner +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: vcp_website_partner +#: model_terms:ir.ui.view,arch_db:vcp_website_partner.partner_detail +msgid "Comments" +msgstr "Commentaires" + +#. module: vcp_website_partner +#: model_terms:ir.ui.view,arch_db:vcp_website_partner.partner_detail +msgid "Created Requests" +msgstr "Requêtes créées" + +#. module: vcp_website_partner +#: model_terms:ir.ui.view,arch_db:vcp_website_partner.partner_detail +msgid "Merged Requests" +msgstr "Requêtes fusionnées" + +#. module: vcp_website_partner +#: model_terms:ir.ui.view,arch_db:vcp_website_partner.partner_detail +msgid "Reviews" +msgstr "Revues" diff --git a/vcp_website_partner/pyproject.toml b/vcp_website_partner/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/vcp_website_partner/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/vcp_website_partner/readme/DESCRIPTION.md b/vcp_website_partner/readme/DESCRIPTION.md new file mode 100644 index 0000000..79a8b60 --- /dev/null +++ b/vcp_website_partner/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module extends the functionality of `website_partner` module, +when `vcp` module is installed. + +It adds on partner website form view, some indicators regarding contributions. + +![website_partner_form](../static/description/website_partner_form.png) diff --git a/vcp_website_partner/static/description/index.html b/vcp_website_partner/static/description/index.html new file mode 100644 index 0000000..5fbbbdc --- /dev/null +++ b/vcp_website_partner/static/description/index.html @@ -0,0 +1,421 @@ + + + + + +Vcp - Website Partner + + + +
+

Vcp - Website Partner

+ + +

Beta License: AGPL-3 OCA/version-control-platform Translate me on Weblate Try me on Runboat

+

This module extends the functionality of website_partner module, +when vcp module is installed.

+

It adds on partner website form view, some indicators regarding +contributions.

+

website_partner_form

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
  • GRAP
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/version-control-platform project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/vcp_website_partner/static/description/website_partner_form.png b/vcp_website_partner/static/description/website_partner_form.png new file mode 100644 index 0000000000000000000000000000000000000000..ad3e358da0b6fa680b8b3f1eb3529a39ba923f83 GIT binary patch literal 45139 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 uOaC8VW)n6@di-kPue<;K;s0$acLHy + + + From 85bde73ebbe71fd9085829e3d287971e9075fc9b Mon Sep 17 00:00:00 2001 From: Sylvain LE GAL Date: Sun, 25 Jan 2026 01:48:13 +0100 Subject: [PATCH 6/7] [REF] vcp: split into vcp and website to make the module more modular. --- vcp/__manifest__.py | 2 +- vcp/models/res_partner.py | 2 - vcp_website/README.rst | 73 ++++ vcp_website/__init__.py | 1 + vcp_website/__manifest__.py | 15 + vcp_website/i18n/fr.po | 20 ++ vcp_website/models/__init__.py | 1 + vcp_website/models/res_partner.py | 14 + vcp_website/pyproject.toml | 3 + vcp_website/readme/DESCRIPTION.md | 2 + vcp_website/static/description/index.html | 418 ++++++++++++++++++++++ 11 files changed, 548 insertions(+), 3 deletions(-) create mode 100644 vcp_website/README.rst create mode 100644 vcp_website/__init__.py create mode 100644 vcp_website/__manifest__.py create mode 100644 vcp_website/i18n/fr.po create mode 100644 vcp_website/models/__init__.py create mode 100644 vcp_website/models/res_partner.py create mode 100644 vcp_website/pyproject.toml create mode 100644 vcp_website/readme/DESCRIPTION.md create mode 100644 vcp_website/static/description/index.html diff --git a/vcp/__manifest__.py b/vcp/__manifest__.py index f0711c4..54b540f 100644 --- a/vcp/__manifest__.py +++ b/vcp/__manifest__.py @@ -8,7 +8,7 @@ "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/version-control-platform", - "depends": ["website"], + "depends": ["portal"], "data": [ "security/ir.model.access.csv", "data/ir_cron.xml", diff --git a/vcp/models/res_partner.py b/vcp/models/res_partner.py index b2794bf..cdecce8 100644 --- a/vcp/models/res_partner.py +++ b/vcp/models/res_partner.py @@ -60,8 +60,6 @@ def _compute_vcp_contributions_field(self, field): ) def _get_contributor_url(self): - if self.is_published and self.website_url: - return self.website_url return False def _get_contributors_name(self, kind, **kwargs): diff --git a/vcp_website/README.rst b/vcp_website/README.rst new file mode 100644 index 0000000..09c8f79 --- /dev/null +++ b/vcp_website/README.rst @@ -0,0 +1,73 @@ +============= +Vcp - Website +============= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0471d7839f1265a582540c4f4e8dd6b37fb02dcc94fc7ae9c1bf9474f8466efc + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fversion--control--platform-lightgray.png?logo=github + :target: https://github.com/OCA/version-control-platform/tree/18.0/vcp_website + :alt: OCA/version-control-platform +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/version-control-platform-18-0/version-control-platform-18-0-vcp_website + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/version-control-platform&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is a technical glue module, installed when ``website`` and +``vcp`` modules are installed. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit +* GRAP + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/version-control-platform `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/vcp_website/__init__.py b/vcp_website/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/vcp_website/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/vcp_website/__manifest__.py b/vcp_website/__manifest__.py new file mode 100644 index 0000000..7df0338 --- /dev/null +++ b/vcp_website/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2026 GRAP +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Vcp - Website", + "summary": "Glue module between Virtual Control Platform and Website", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,GRAP,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/version-control-platform", + "depends": ["vcp", "website"], + "data": [], + "demo": [], + "auto_install": True, +} diff --git a/vcp_website/i18n/fr.po b/vcp_website/i18n/fr.po new file mode 100644 index 0000000..c28badf --- /dev/null +++ b/vcp_website/i18n/fr.po @@ -0,0 +1,20 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * vcp_website +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: vcp_website +#: model:ir.model,name:vcp_website.model_res_partner +msgid "Contact" +msgstr "Contact" diff --git a/vcp_website/models/__init__.py b/vcp_website/models/__init__.py new file mode 100644 index 0000000..91fed54 --- /dev/null +++ b/vcp_website/models/__init__.py @@ -0,0 +1 @@ +from . import res_partner diff --git a/vcp_website/models/res_partner.py b/vcp_website/models/res_partner.py new file mode 100644 index 0000000..e4a69ef --- /dev/null +++ b/vcp_website/models/res_partner.py @@ -0,0 +1,14 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + def _get_contributor_url(self): + if self.is_published and self.website_url: + return self.website_url + return super()._get_contributor_url() diff --git a/vcp_website/pyproject.toml b/vcp_website/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/vcp_website/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/vcp_website/readme/DESCRIPTION.md b/vcp_website/readme/DESCRIPTION.md new file mode 100644 index 0000000..24d3a63 --- /dev/null +++ b/vcp_website/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module is a technical glue module, +installed when `website` and `vcp` modules are installed. diff --git a/vcp_website/static/description/index.html b/vcp_website/static/description/index.html new file mode 100644 index 0000000..e882a72 --- /dev/null +++ b/vcp_website/static/description/index.html @@ -0,0 +1,418 @@ + + + + + +Vcp - Website + + + +
+

Vcp - Website

+ + +

Beta License: AGPL-3 OCA/version-control-platform Translate me on Weblate Try me on Runboat

+

This module is a technical glue module, installed when website and +vcp modules are installed.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
  • GRAP
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/version-control-platform project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From a261444a9cbca67e44eb77738ce9883cfa137e6a Mon Sep 17 00:00:00 2001 From: Sylvain LE GAL Date: Sun, 25 Jan 2026 15:01:28 +0100 Subject: [PATCH 7/7] [REF] vcp: split into vcp and portal to make the module more modular. --- vcp/__init__.py | 1 - vcp/__manifest__.py | 13 +- vcp/tests/__init__.py | 2 +- vcp/tests/{test_portal.py => test_base.py} | 5 +- vcp_portal/README.rst | 85 ++++ vcp_portal/__init__.py | 1 + vcp_portal/__manifest__.py | 27 ++ {vcp => vcp_portal}/controllers/__init__.py | 0 {vcp => vcp_portal}/controllers/main.py | 4 +- vcp_portal/i18n/fr.po | 15 + vcp_portal/pyproject.toml | 3 + vcp_portal/readme/DESCRIPTION.md | 10 + vcp_portal/static/description/index.html | 423 ++++++++++++++++++ vcp_portal/static/description/portal_menu.png | Bin 0 -> 45139 bytes .../popover_tooltip/popover_tooltip.esm.js | 0 .../popover_tooltip/popover_tooltip.xml | 0 .../components/vcp_render/vcp_render.esm.js | 0 .../src/components/vcp_render/vcp_render.scss | 0 .../src/components/vcp_render/vcp_render.xml | 0 .../static/tests/tours/portal.esm.js | 0 {vcp => vcp_portal}/templates/templates.xml | 0 vcp_portal/tests/__init__.py | 1 + vcp_portal/tests/test_vcp_portal.py | 13 + 23 files changed, 583 insertions(+), 20 deletions(-) rename vcp/tests/{test_portal.py => test_base.py} (95%) create mode 100644 vcp_portal/README.rst create mode 100644 vcp_portal/__init__.py create mode 100644 vcp_portal/__manifest__.py rename {vcp => vcp_portal}/controllers/__init__.py (100%) rename {vcp => vcp_portal}/controllers/main.py (98%) create mode 100644 vcp_portal/i18n/fr.po create mode 100644 vcp_portal/pyproject.toml create mode 100644 vcp_portal/readme/DESCRIPTION.md create mode 100644 vcp_portal/static/description/index.html create mode 100644 vcp_portal/static/description/portal_menu.png rename {vcp => vcp_portal}/static/src/components/popover_tooltip/popover_tooltip.esm.js (100%) rename {vcp => vcp_portal}/static/src/components/popover_tooltip/popover_tooltip.xml (100%) rename {vcp => vcp_portal}/static/src/components/vcp_render/vcp_render.esm.js (100%) rename {vcp => vcp_portal}/static/src/components/vcp_render/vcp_render.scss (100%) rename {vcp => vcp_portal}/static/src/components/vcp_render/vcp_render.xml (100%) rename {vcp => vcp_portal}/static/tests/tours/portal.esm.js (100%) rename {vcp => vcp_portal}/templates/templates.xml (100%) create mode 100644 vcp_portal/tests/__init__.py create mode 100644 vcp_portal/tests/test_vcp_portal.py diff --git a/vcp/__init__.py b/vcp/__init__.py index 91c5580..0650744 100644 --- a/vcp/__init__.py +++ b/vcp/__init__.py @@ -1,2 +1 @@ -from . import controllers from . import models diff --git a/vcp/__manifest__.py b/vcp/__manifest__.py index 54b540f..0d061e0 100644 --- a/vcp/__manifest__.py +++ b/vcp/__manifest__.py @@ -8,11 +8,10 @@ "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/version-control-platform", - "depends": ["portal"], + "depends": ["base"], "data": [ "security/ir.model.access.csv", "data/ir_cron.xml", - "templates/templates.xml", "views/menu.xml", "views/vcp_comment.xml", "views/vcp_review.xml", @@ -22,14 +21,4 @@ "views/vcp_platform.xml", ], "demo": [], - "assets": { - "web.assets_frontend": [ - "vcp/static/src/components/**/*.esm.js", - "vcp/static/src/components/**/*.xml", - "vcp/static/src/components/**/*.scss", - ], - "web.assets_tests": [ - "vcp/static/tests/**/*", - ], - }, } diff --git a/vcp/tests/__init__.py b/vcp/tests/__init__.py index 8307da4..02bd456 100644 --- a/vcp/tests/__init__.py +++ b/vcp/tests/__init__.py @@ -1 +1 @@ -from . import test_portal +from . import test_base diff --git a/vcp/tests/test_portal.py b/vcp/tests/test_base.py similarity index 95% rename from vcp/tests/test_portal.py rename to vcp/tests/test_base.py index d47ea73..05117da 100644 --- a/vcp/tests/test_portal.py +++ b/vcp/tests/test_base.py @@ -12,7 +12,7 @@ @tagged("post_install", "-at_install") -class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal): +class TestBase(HttpCaseWithUserDemo, HttpCaseWithUserPortal): @classmethod def setUpClass(cls): super().setUpClass() @@ -130,6 +130,3 @@ def setUpClass(cls): "created_at": date, } ) - - def test_01_portal_load_tour(self): - self.start_tour("/", "portal_load_contributors_github", login="portal") diff --git a/vcp_portal/README.rst b/vcp_portal/README.rst new file mode 100644 index 0000000..e3cac0f --- /dev/null +++ b/vcp_portal/README.rst @@ -0,0 +1,85 @@ +============ +Vcp - Portal +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:649a33ecc95d6699ddbe80c0b0781a81f36d66c4b09839dc8f133a4127369bb7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fversion--control--platform-lightgray.png?logo=github + :target: https://github.com/OCA/version-control-platform/tree/18.0/vcp_portal + :alt: OCA/version-control-platform +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/version-control-platform-18-0/version-control-platform-18-0-vcp_portal + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/version-control-platform&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of ``portal`` module, when ``vcp`` +module is installed. + +It adds a new entry in the partner portal menu. + +|portal_menu| + +When selecting this new item, partner has the possibility to see, some +indicators regarding contributions. + +|portal_form| + +.. |portal_menu| image:: https://raw.githubusercontent.com/OCA/version-control-platform/18.0/vcp_portal/static/description/portal_menu.png +.. |portal_form| image:: https://raw.githubusercontent.com/OCA/version-control-platform/18.0/vcp_portal/static/description/portal_form.png + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit +* GRAP + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/version-control-platform `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/vcp_portal/__init__.py b/vcp_portal/__init__.py new file mode 100644 index 0000000..e046e49 --- /dev/null +++ b/vcp_portal/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/vcp_portal/__manifest__.py b/vcp_portal/__manifest__.py new file mode 100644 index 0000000..cfa9d1c --- /dev/null +++ b/vcp_portal/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2026 GRAP +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Vcp - Portal", + "summary": "Glue module between Virtual Control Platform and Portal", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,GRAP,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/version-control-platform", + "depends": ["vcp", "portal"], + "data": [ + "templates/templates.xml", + ], + "demo": [], + "assets": { + "web.assets_frontend": [ + "vcp_portal/static/src/components/**/*.esm.js", + "vcp_portal/static/src/components/**/*.xml", + "vcp_portal/static/src/components/**/*.scss", + ], + "web.assets_tests": [ + "vcp_portal/static/tests/**/*", + ], + }, + "auto_install": True, +} diff --git a/vcp/controllers/__init__.py b/vcp_portal/controllers/__init__.py similarity index 100% rename from vcp/controllers/__init__.py rename to vcp_portal/controllers/__init__.py diff --git a/vcp/controllers/main.py b/vcp_portal/controllers/main.py similarity index 98% rename from vcp/controllers/main.py rename to vcp_portal/controllers/main.py index f9b2080..a2283a1 100644 --- a/vcp/controllers/main.py +++ b/vcp_portal/controllers/main.py @@ -25,7 +25,7 @@ def contributors_vcp(self, vcp=None): if vcp is None: vcps = request.env["vcp.platform"].search([]) return request.render( - "vcp.vcp_platforms_template", + "vcp_portal.vcp_platforms_template", {"vcps": vcps, **values}, ) vcp_id = ( @@ -35,7 +35,7 @@ def contributors_vcp(self, vcp=None): .id ) return request.render( - "vcp.vcp_platform_template", + "vcp_portal.vcp_platform_template", {"vcp": vcp_id, **values}, ) diff --git a/vcp_portal/i18n/fr.po b/vcp_portal/i18n/fr.po new file mode 100644 index 0000000..80b08c3 --- /dev/null +++ b/vcp_portal/i18n/fr.po @@ -0,0 +1,15 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * vcp_website_portal +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/vcp_portal/pyproject.toml b/vcp_portal/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/vcp_portal/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/vcp_portal/readme/DESCRIPTION.md b/vcp_portal/readme/DESCRIPTION.md new file mode 100644 index 0000000..2b3a7fc --- /dev/null +++ b/vcp_portal/readme/DESCRIPTION.md @@ -0,0 +1,10 @@ +This module extends the functionality of `portal` module, +when `vcp` module is installed. + +It adds a new entry in the partner portal menu. + +![portal_menu](../static/description/portal_menu.png) + +When selecting this new item, partner has the possibility to see, some indicators regarding contributions. + +![portal_form](../static/description/portal_form.png) diff --git a/vcp_portal/static/description/index.html b/vcp_portal/static/description/index.html new file mode 100644 index 0000000..cbc57c4 --- /dev/null +++ b/vcp_portal/static/description/index.html @@ -0,0 +1,423 @@ + + + + + +Vcp - Portal + + + +
+

Vcp - Portal

+ + +

Beta License: AGPL-3 OCA/version-control-platform Translate me on Weblate Try me on Runboat

+

This module extends the functionality of portal module, when vcp +module is installed.

+

It adds a new entry in the partner portal menu.

+

portal_menu

+

When selecting this new item, partner has the possibility to see, some +indicators regarding contributions.

+

portal_form

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
  • GRAP
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/version-control-platform project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/vcp_portal/static/description/portal_menu.png b/vcp_portal/static/description/portal_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..ad3e358da0b6fa680b8b3f1eb3529a39ba923f83 GIT binary patch literal 45139 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 uOaC8VW)n6@di-kPue<;K;s0$acLHy