From 31fa9d0ff466ebe97e28091d057a43950fc70054 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 5 Jan 2026 09:15:11 +0100 Subject: [PATCH 01/44] [ADD] contributors_github --- contributors_github/README.rst | 32 +++ contributors_github/__init__.py | 2 + contributors_github/__manifest__.py | 35 +++ contributors_github/controllers/__init__.py | 1 + contributors_github/controllers/main.py | 192 ++++++++++++++ contributors_github/data/ir_cron.xml | 14 + contributors_github/models/__init__.py | 7 + .../models/contributors_branch.py | 33 +++ .../models/contributors_comment.py | 36 +++ .../models/contributors_organization.py | 86 ++++++ .../models/contributors_pull_request.py | 68 +++++ .../models/contributors_repository.py | 249 ++++++++++++++++++ .../models/contributors_review.py | 33 +++ contributors_github/models/res_partner.py | 88 +++++++ contributors_github/pyproject.toml | 3 + contributors_github/readme/CONTRIBUTORS.md | 2 + contributors_github/readme/DESCRIPTIONS.md | 1 + contributors_github/readme/USAGE.md | 7 + .../security/ir.model.access.csv | 11 + .../static/description/icon.png | Bin 0 -> 2789 bytes .../static/description/icon.svg | 75 ++++++ .../contributors_render.esm.js | 121 +++++++++ .../contributors_render.scss | 9 + .../contributors_render.xml | 197 ++++++++++++++ contributors_github/templates/templates.xml | 76 ++++++ contributors_github/tests/__init__.py | 1 + contributors_github/tests/test_github.py | 117 ++++++++ .../views/contributors_branch.xml | 27 ++ .../views/contributors_comment.xml | 26 ++ .../views/contributors_organization.xml | 81 ++++++ .../views/contributors_pull_request.xml | 94 +++++++ .../views/contributors_repository.xml | 102 +++++++ .../views/contributors_review.xml | 27 ++ contributors_github/views/menu.xml | 11 + contributors_github/views/res_partner.xml | 27 ++ requirements.txt | 3 + 36 files changed, 1894 insertions(+) create mode 100644 contributors_github/README.rst create mode 100644 contributors_github/__init__.py create mode 100644 contributors_github/__manifest__.py create mode 100644 contributors_github/controllers/__init__.py create mode 100644 contributors_github/controllers/main.py create mode 100644 contributors_github/data/ir_cron.xml create mode 100644 contributors_github/models/__init__.py create mode 100644 contributors_github/models/contributors_branch.py create mode 100644 contributors_github/models/contributors_comment.py create mode 100644 contributors_github/models/contributors_organization.py create mode 100644 contributors_github/models/contributors_pull_request.py create mode 100644 contributors_github/models/contributors_repository.py create mode 100644 contributors_github/models/contributors_review.py create mode 100644 contributors_github/models/res_partner.py create mode 100644 contributors_github/pyproject.toml create mode 100644 contributors_github/readme/CONTRIBUTORS.md create mode 100644 contributors_github/readme/DESCRIPTIONS.md create mode 100644 contributors_github/readme/USAGE.md create mode 100644 contributors_github/security/ir.model.access.csv create mode 100644 contributors_github/static/description/icon.png create mode 100644 contributors_github/static/description/icon.svg create mode 100644 contributors_github/static/src/components/contributors_render/contributors_render.esm.js create mode 100644 contributors_github/static/src/components/contributors_render/contributors_render.scss create mode 100644 contributors_github/static/src/components/contributors_render/contributors_render.xml create mode 100644 contributors_github/templates/templates.xml create mode 100644 contributors_github/tests/__init__.py create mode 100644 contributors_github/tests/test_github.py create mode 100644 contributors_github/views/contributors_branch.xml create mode 100644 contributors_github/views/contributors_comment.xml create mode 100644 contributors_github/views/contributors_organization.xml create mode 100644 contributors_github/views/contributors_pull_request.xml create mode 100644 contributors_github/views/contributors_repository.xml create mode 100644 contributors_github/views/contributors_review.xml create mode 100644 contributors_github/views/menu.xml create mode 100644 contributors_github/views/res_partner.xml create mode 100644 requirements.txt diff --git a/contributors_github/README.rst b/contributors_github/README.rst new file mode 100644 index 0000000..69bbc7f --- /dev/null +++ b/contributors_github/README.rst @@ -0,0 +1,32 @@ +=================== +Contributors Github +=================== + +Get Contributors and Contributions from Github + +Purpose +======= + +This module does this and that... + +Explain the use case. + +Configuration +============= + +To configure this module, you need to: + +#. Go to ... + +Usage +===== + +To use this module, you need to: + +#. Go to ... + + +How to test +=========== + +... diff --git a/contributors_github/__init__.py b/contributors_github/__init__.py new file mode 100644 index 0000000..91c5580 --- /dev/null +++ b/contributors_github/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/contributors_github/__manifest__.py b/contributors_github/__manifest__.py new file mode 100644 index 0000000..dfb7a3c --- /dev/null +++ b/contributors_github/__manifest__.py @@ -0,0 +1,35 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Contributors Github", + "summary": """Get Contributors and Contributions from Github""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit, Odoo Community Association (OCA)", + "website": "https://github.com/OCA-contributors/contributors-module", + "maintainers": ["etobella"], + "depends": ["portal"], + "data": [ + "security/ir.model.access.csv", + "views/menu.xml", + "views/contributors_comment.xml", + "views/contributors_review.xml", + "views/contributors_branch.xml", + "views/contributors_pull_request.xml", + "views/contributors_repository.xml", + "views/contributors_organization.xml", + "views/res_partner.xml", + "data/ir_cron.xml", + "templates/templates.xml", + ], + "external_dependencies": {"python": ["github3.py", "PyYAML"]}, + "demo": [], + "assets": { + "web.assets_frontend": [ + "contributors_github/static/src/components/**/*.esm.js", + "contributors_github/static/src/components/**/*.xml", + "contributors_github/static/src/components/**/*.scss", + ], + }, +} diff --git a/contributors_github/controllers/__init__.py b/contributors_github/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/contributors_github/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/contributors_github/controllers/main.py b/contributors_github/controllers/main.py new file mode 100644 index 0000000..8386dac --- /dev/null +++ b/contributors_github/controllers/main.py @@ -0,0 +1,192 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from collections import defaultdict +from datetime import datetime +from math import sqrt + +from dateutil.relativedelta import relativedelta + +from odoo import http +from odoo.http import request + +from odoo.addons.portal.controllers.portal import CustomerPortal + + +class ContributorsController(CustomerPortal): + @http.route( + [ + "/contributors", + "/contributors/", + ], + type="http", + auth="user", + website=True, + ) + def contributors_organization(self, organization=None): + values = self._prepare_portal_layout_values() + values.update(self._prepare_home_portal_values([])) + if organization is None: + organizations = request.env["contributors.organization"].search([]) + return request.render( + "contributors_github.contributors_template", + {"organizations": organizations, **values}, + ) + organization_id = ( + request.env["contributors.organization"] + .sudo() + .search([("name", "=", organization)], limit=1) + .id + ) + return request.render( + "contributors_github.contributors_organization_template", + {"organization": organization_id, **values}, + ) + + 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 + + def _get_merged_domain(self, organization, start, end, **values): + return [ + ("repository_id.organization_id", "=", organization.id), + ("is_merged", "=", True), + ("closed_at", ">=", start), + ("closed_at", "<", end), + ] + + def _get_created_domain(self, organization, start, end, **values): + return [ + ("repository_id.organization_id", "=", organization.id), + ("created_at", ">=", start), + ("created_at", "<", end), + ] + + def _get_comments_domain(self, organization, start, end, **values): + return [ + ("pull_request_id.repository_id.organization_id", "=", organization.id), + ("created_at", ">=", start), + ("created_at", "<", end), + ] + + def _get_reviews_domain(self, organization, start, end, **values): + return [ + ("pull_request_id.repository_id.organization_id", "=", organization.id), + ("submitted_at", ">=", start), + ("submitted_at", "<", end), + ] + + def _get_index(self, data): + return round( + sqrt(data["created_pull_requests"]) + + data["merged_pull_requests"] + + sqrt(data["comments"]) + + data["reviews"], + 2, + ) + + @http.route(["/contributors/fetch"], type="json", auth="user", readonly=True) + def fetch_data(self, organization_id, year, month, kind, period, **values): + start, end = self._get_dates(year, month, period, **values) + organization = ( + request.env["contributors.organization"].browse(organization_id).exists() + ) + if not organization: + return [] + data = defaultdict( + lambda: { + "name": "", + "github_name": "", + "index": 0, + "created_pull_requests": 0, + "merged_pull_requests": 0, + "comments": 0, + "reviews": 0, + } + ) + if kind == "contributors": + field = "partner_id" + elif kind == "organizations": + field = "organization_id" + elif kind == "repositories": + field = "repository_id" + else: + return [] + + for merged in ( + request.env["contributors.pull.request"] + .sudo() + .read_group( + self._get_merged_domain(organization, start, end, **values) + + [(field, "!=", False)], + [field], + [field], + ) + ): + data[merged[field][0]]["merged_pull_requests"] = merged[f"{field}_count"] + for pr in ( + request.env["contributors.pull.request"] + .sudo() + .read_group( + self._get_created_domain(organization, start, end, **values) + + [(field, "!=", False)], + [field], + [field], + ) + ): + data[pr[field][0]]["created_pull_requests"] = pr[f"{field}_count"] + for comment in ( + request.env["contributors.comment"] + .sudo() + .read_group( + self._get_comments_domain(organization, start, end, **values) + + [(field, "!=", False)], + [field], + [field], + ) + ): + data[comment[field][0]]["comments"] = comment[f"{field}_count"] + for review in ( + request.env["contributors.review"] + .sudo() + .read_group( + self._get_reviews_domain(organization, start, end, **values) + + [(field, "!=", False)], + [field], + [field], + ) + ): + data[review[field][0]]["reviews"] = review[f"{field}_count"] + return self._improve_data(data, kind, **values) + + def _improve_data(self, data, kind, **kwargs): + for key, values in data.items(): + if kind == "contributors": + partner = request.env["res.partner"].browse(key) + values["name"] = self._get_partner_name(partner, **kwargs) + values["github_name"] = partner.github_name + values["index"] = self._get_index(values) + elif kind == "organizations": + organization = request.env["res.partner"].browse(key) + values["name"] = organization.name + values["github_name"] = organization.github_name or organization.name + elif kind == "repositories": + repository = request.env["contributors.repository"].browse(key) + values["name"] = repository.name + values["github_name"] = ( + f"{repository.organization_id.name}/{repository.name}" + ) + values["index"] = self._get_index(values) + return data + + def _get_partner_name(self, partner, **kwargs): + return partner.name diff --git a/contributors_github/data/ir_cron.xml b/contributors_github/data/ir_cron.xml new file mode 100644 index 0000000..5a758c2 --- /dev/null +++ b/contributors_github/data/ir_cron.xml @@ -0,0 +1,14 @@ + + + + + GitHub Repository Update + + code + model._cron_update_repositories() + 1 + minutes + False + + diff --git a/contributors_github/models/__init__.py b/contributors_github/models/__init__.py new file mode 100644 index 0000000..2cccabd --- /dev/null +++ b/contributors_github/models/__init__.py @@ -0,0 +1,7 @@ +from . import res_partner +from . import contributors_organization +from . import contributors_repository +from . import contributors_pull_request +from . import contributors_branch +from . import contributors_review +from . import contributors_comment diff --git a/contributors_github/models/contributors_branch.py b/contributors_github/models/contributors_branch.py new file mode 100644 index 0000000..8d85766 --- /dev/null +++ b/contributors_github/models/contributors_branch.py @@ -0,0 +1,33 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ContributorsBranch(models.Model): + _name = "contributors.branch" + _description = "Contributors Branch" # TODO + + name = fields.Char(required=True) + organization_id = fields.Many2one( + comodel_name="contributors.organization", + string="Organization", + required=True, + ) + _sql_constraints = [ + ("name_uniq", "unique(name, organization_id)", "Branch name must be unique.") + ] + + def _get_branch(self, organization, name): + branch = self.search( + [("organization_id", "=", organization.id), ("name", "=", name)], + limit=1, + ) + if not branch: + branch = self.sudo().create( + { + "organization_id": organization.id, + "name": name, + } + ) + return branch.id diff --git a/contributors_github/models/contributors_comment.py b/contributors_github/models/contributors_comment.py new file mode 100644 index 0000000..82a6539 --- /dev/null +++ b/contributors_github/models/contributors_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 ContributorsComment(models.Model): + _name = "contributors.comment" + _description = "Contributors Comment" # TODO + + github_id = fields.Char(string="GitHub ID", readonly=True, required=True) + body = fields.Html(readonly=True) + partner_id = fields.Many2one( + comodel_name="res.partner", + readonly=True, + ) + organization_id = fields.Many2one( + related="pull_request_id.organization_id", + readonly=True, + store=True, + ) + repository_id = fields.Many2one( + related="pull_request_id.repository_id", + readonly=True, + store=True, + ) + created_at = fields.Datetime(readonly=True) + updated_at = fields.Datetime(readonly=True) + pull_request_id = fields.Many2one( + comodel_name="contributors.pull.request", + string="Pull Request", + readonly=True, + ) + _sql_constraints = [ + ("github_id_uniq", "unique(github_id)", "GitHub ID must be unique.") + ] diff --git a/contributors_github/models/contributors_organization.py b/contributors_github/models/contributors_organization.py new file mode 100644 index 0000000..3dda1eb --- /dev/null +++ b/contributors_github/models/contributors_organization.py @@ -0,0 +1,86 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 + +import github3 +import requests + +from odoo import fields, models + + +class ContributorsOrganization(models.Model): + _name = "contributors.organization" + _description = "Contributors Organization" # TODO + + name = fields.Char(required=True) + description = fields.Char(readonly=True) + short_description = fields.Char(readonly=True) + branch_ids = fields.One2many( + comodel_name="contributors.branch", + inverse_name="organization_id", + string="Branches", + readonly=True, + ) + repository_ids = fields.One2many( + comodel_name="contributors.repository", + inverse_name="organization_id", + string="Repositories", + readonly=True, + ) + key_ids = fields.One2many( + comodel_name="contributors.organization.key", + inverse_name="organization_id", + string="API Keys", + ) + last_update = fields.Datetime(readonly=True) + active = fields.Boolean(default=True) + update_interval_days = fields.Integer(default=3) + image_1920 = fields.Image() + 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" + ) + + def _get_clients(self): + git = [] + for key in self.key_ids: + git.append(github3.login(token=key.name)) + return git + + def update_information(self): + self.ensure_one() + clients = self._get_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.env["contributors.repository"]._update_repository(repo, self) + self.last_update = fields.Datetime.now() + + +class ContributorsOrganizationKey(models.Model): + _name = "contributors.organization.key" + _description = "Contributors Organization API Key" # TODO + + organization_id = fields.Many2one( + comodel_name="contributors.organization", + string="Organization", + required=True, + ondelete="cascade", + ) + name = fields.Char(required=True) + + _sql_constraints = [ + ("name_uniq", "unique(name, organization_id)", "API Key must be unique.") + ] diff --git a/contributors_github/models/contributors_pull_request.py b/contributors_github/models/contributors_pull_request.py new file mode 100644 index 0000000..f882172 --- /dev/null +++ b/contributors_github/models/contributors_pull_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 ContributorsPullRequest(models.Model): + _name = "contributors.pull.request" + _description = "Contributors Pull Request" # TODO + + github_id = fields.Char(string="GitHub ID", readonly=True) + name = fields.Char(readonly=True) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Contributor", + readonly=True, + ) + repository_id = fields.Many2one( + comodel_name="contributors.repository", + readonly=True, + ondelete="cascade", + ) + branch_id = fields.Many2one( + comodel_name="contributors.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="contributors.pull.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 = [ + ("github_id_uniq", "unique(github_id)", "GitHub ID must be unique.") + ] + + +class ContributorsPullRequestLabel(models.Model): + _name = "contributors.pull.request.label" + _description = "Contributors Pull Request Label" # TODO + + name = fields.Char(required=True) + + _sql_constraints = [("name_uniq", "unique(name)", "Label name must be unique.")] + + @tools.ormcache() + 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/contributors_github/models/contributors_repository.py b/contributors_github/models/contributors_repository.py new file mode 100644 index 0000000..f6b7487 --- /dev/null +++ b/contributors_github/models/contributors_repository.py @@ -0,0 +1,249 @@ +# 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 api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class ContributorsRepository(models.Model): + _name = "contributors.repository" + _description = "Contributors Repository" + + name = fields.Char(required=True) + description = fields.Char(readonly=True) + organization_id = fields.Many2one( + comodel_name="contributors.organization", + string="Organization", + 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) + pull_request_ids = fields.One2many( + "contributors.pull.request", inverse_name="repository_id" + ) + pull_request_count = fields.Integer(compute="_compute_pull_request_count") + active = fields.Boolean(default=True) + + @api.depends("pull_request_ids") + def _compute_pull_request_count(self): + for record in self: + record.pull_request_count = len(record.pull_request_ids) + + def parse_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.env["contributors.branch"]._get_branch( + self.organization_id, 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._parse_date(origin_data["created_at"]), + "closed_at": self._parse_date(origin_data["closed_at"]), + "number": pr.number, + "updated_at": self._parse_date(origin_data["updated_at"]), + "label_ids": [fields.Command.clear()] + + [ + fields.Command.link( + self.env["contributors.pull.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._parse_date(c["created_at"]), + "updated_at": self._parse_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._parse_date(r.get("submitted_at")), + "state": r["state"]["keyword"] + if isinstance(r["state"], dict) + else r["state"], + } + for r in reviews + ], + ) + + def force_update_information(self): + self.update_information(update_interval_days=365) + + def update_information(self, update_interval_days=None): + self.ensure_one() + clients = self.organization_id._get_clients() + try: + start = UTC.localize(self.from_date) + end = min( + start + + timedelta( + days=update_interval_days + or self.organization_id.update_interval_days + ), + UTC.localize(datetime.now()), + ) + start += timedelta( + days=-1 + ) # Add buffer day to avoid missing PRs on boundary dates + i = 0 + for pr in clients[0].search_issues( + f"is:pr repo:{self.organization_id.name}/{self.name} " + f"updated:{start.isoformat()}..{end.isoformat()}" + ): + i = (1 + i) % len(clients) + pr_id, pr_data, comments, reviews = self.parse_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["contributors.pull.request"].search( + [("github_id", "=", pr_id)], limit=1 + ) + if not opr: + opr = ( + self.env["contributors.pull.request"] + .sudo() + .create({"github_id": pr_id, **pr_data}) + ) + else: + opr.sudo().write(pr_data) + for comment in comments: + comment_id = comment.pop("id") + ocomment = self.env["contributors.comment"].search( + [("github_id", "=", comment_id)], limit=1 + ) + if not ocomment: + self.env["contributors.comment"].sudo().create( + { + "github_id": comment_id, + "pull_request_id": opr.id, + **comment, + } + ) + else: + ocomment.sudo().write(comment) + for review in reviews: + review_id = review.pop("id") + oreview = self.env["contributors.review"].search( + [("github_id", "=", review_id)], limit=1 + ) + if not oreview: + self.env["contributors.review"].sudo().create( + { + "github_id": review_id, + "pull_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[0].rate_limit() + reset = fields.Datetime.to_string( + datetime.utcfromtimestamp(rate["resources"]["core"]["reset"]) + ) + raise ValidationError(self.env._(f"Reset on {reset}")) from e + + def _parse_date(self, date): + if not date: + return False + return UTC.normalize( + datetime.fromisoformat(date.replace("Z", "+00:00")) + ).replace(tzinfo=None) + + def _update_repository(self, repo, organization): + vals = { + "created_at": self._parse_date(repo.created_at), + "stargazers_count": repo.stargazers_count, + "fork_count": repo.forks_count, + "watchers_count": repo.watchers_count, + "description": repo.description, + } + repository = self.search( + [ + ("name", "=", repo.name), + ("organization_id", "=", organization.id), + ], + limit=1, + ) + if not repository: + repository = ( + self.env["contributors.repository"] + .sudo() + .create( + { + "name": repo.name, + "organization_id": organization.id, + "from_date": vals.get("created_at"), + **vals, + } + ) + ) + else: + repository.sudo().write(vals) + + def _cron_update_repositories(self, limit=1): + repositories = self.search([], limit=limit, order="from_date ASC") + for repository in repositories: + repository.update_information() diff --git a/contributors_github/models/contributors_review.py b/contributors_github/models/contributors_review.py new file mode 100644 index 0000000..453f3d8 --- /dev/null +++ b/contributors_github/models/contributors_review.py @@ -0,0 +1,33 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ContributorsReview(models.Model): + _name = "contributors.review" + _description = "Contributors Review" # TODO + + github_id = fields.Char(string="GitHub ID", readonly=True, required=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) + pull_request_id = fields.Many2one( + "contributors.pull.request", + readonly=True, + ) + organization_id = fields.Many2one( + related="pull_request_id.organization_id", + readonly=True, + store=True, + ) + repository_id = fields.Many2one( + related="pull_request_id.repository_id", + readonly=True, + store=True, + ) + + _sql_constraints = [ + ("github_id_uniq", "unique(github_id)", "GitHub ID must be unique.") + ] diff --git a/contributors_github/models/res_partner.py b/contributors_github/models/res_partner.py new file mode 100644 index 0000000..a14da2b --- /dev/null +++ b/contributors_github/models/res_partner.py @@ -0,0 +1,88 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import github3 + +from odoo import fields, models + + +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.", + ), + ] + + def _get_github_user(self, gh, client): + if not gh: + return False + partner = self.with_context(active_test=False).search( + [("github_name", "=ilike", str(gh))], limit=1 + ) + if not partner: + if isinstance(gh, str): + gh = client.user(str(gh)) + return self.create( + { + "name": getattr(gh, "name", None) or str(gh), + "github_name": str(gh), + "github_user": True, + } + ).id + if not partner.github_user: + partner.github_user = True + return partner.id + + def _get_github_organization(self, gh, client): + if not gh: + return False + partner = self.with_context(active_test=False).search( + [("github_name", "=ilike", str(gh))], limit=1 + ) + if not partner: + try: + org = client.organization(str(gh)) + if org: + 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 + if not partner.github_organization: + partner.github_organization = True + return partner.id diff --git a/contributors_github/pyproject.toml b/contributors_github/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/contributors_github/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/contributors_github/readme/CONTRIBUTORS.md b/contributors_github/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..2c066ba --- /dev/null +++ b/contributors_github/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Dixmit](https://www.dixmit.com) + - Enric Tobella diff --git a/contributors_github/readme/DESCRIPTIONS.md b/contributors_github/readme/DESCRIPTIONS.md new file mode 100644 index 0000000..24b67f4 --- /dev/null +++ b/contributors_github/readme/DESCRIPTIONS.md @@ -0,0 +1 @@ +This module allows to get data from github and show it in your odoo instance. diff --git a/contributors_github/readme/USAGE.md b/contributors_github/readme/USAGE.md new file mode 100644 index 0000000..0f17fc5 --- /dev/null +++ b/contributors_github/readme/USAGE.md @@ -0,0 +1,7 @@ +Access the Contributors / Organization module. + +Add your organization and one ore more API Keys (it might be faster if you add several ones). + +Update the repositories. + +The system will start fetching the data automatically. diff --git a/contributors_github/security/ir.model.access.csv b/contributors_github/security/ir.model.access.csv new file mode 100644 index 0000000..eaf4351 --- /dev/null +++ b/contributors_github/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_organization,Access Organization,model_contributors_organization,base.group_user,1,0,0,0 +access_organization_portal,Access Organization,model_contributors_organization,base.group_portal,1,0,0,0 +manage_organization,Manage Organization,model_contributors_organization,base.group_system,1,1,1,0 +manage_organization_key,Manage Organization Keys,model_contributors_organization_key,base.group_system,1,1,1,1 +access_branch,Access Branch,model_contributors_branch,base.group_user,1,0,0,0 +access_repository,Access Repository,model_contributors_repository,base.group_user,1,0,0,0 +access_pull_request,Access Pull Requests,model_contributors_pull_request,base.group_user,1,0,0,0 +access_pull_request_label,Access Pull Requests Labels,model_contributors_pull_request_label,base.group_user,1,0,0,0 +access_review,Access Reviews,model_contributors_review,base.group_user,1,0,0,0 +access_comment,Access Comments,model_contributors_comment,base.group_user,1,0,0,0 diff --git a/contributors_github/static/description/icon.png b/contributors_github/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/contributors_github/static/description/icon.svg b/contributors_github/static/description/icon.svg new file mode 100644 index 0000000..261d62c --- /dev/null +++ b/contributors_github/static/description/icon.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/contributors_github/static/src/components/contributors_render/contributors_render.esm.js b/contributors_github/static/src/components/contributors_render/contributors_render.esm.js new file mode 100644 index 0000000..073e7f4 --- /dev/null +++ b/contributors_github/static/src/components/contributors_render/contributors_render.esm.js @@ -0,0 +1,121 @@ +import {Component, onMounted, useState} from "@odoo/owl"; + +import {Dropdown} from "@web/core/dropdown/dropdown"; +import {DropdownItem} from "@web/core/dropdown/dropdown_item"; +import {formatFloat} from "@web/core/utils/numbers"; +import {registry} from "@web/core/registry"; +import {rpc} from "@web/core/network/rpc"; + +/** + * This Component is a signature request form. It uses + * @see NameAndSignature for the input fields, adds a submit + * button, and handles the RPC to save the result. + */ +export class ContributorsRender extends Component { + static template = "cotributors_github.ContributorsRender"; + 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", + }, + 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)); + } + selectPeriod(period) { + this.state.period = period; + this.fetchData(); + } + async fetchData() { + if (this.state.kind === "contributors") { + this.state.contributors = await rpc( + "/contributors/fetch", + this.getParameters() + ); + } else if (this.state.kind === "organizations") { + this.state.organizations = await rpc( + "/contributors/fetch", + this.getParameters() + ); + } else if (this.state.kind === "repositories") { + this.state.repositories = await rpc( + "/contributors/fetch", + this.getParameters() + ); + } + } + getParameters() { + return { + year: parseInt(this.state.year, 10), + month: parseInt(this.state.month, 10), + organization_id: this.props.organization, + 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; + } +} + +ContributorsRender.props = { + organization: Number, +}; +ContributorsRender.components = { + Dropdown, + DropdownItem, +}; +registry + .category("public_components") + .add("contributors_github.ContributorsRender", ContributorsRender); diff --git a/contributors_github/static/src/components/contributors_render/contributors_render.scss b/contributors_github/static/src/components/contributors_render/contributors_render.scss new file mode 100644 index 0000000..02ec11a --- /dev/null +++ b/contributors_github/static/src/components/contributors_render/contributors_render.scss @@ -0,0 +1,9 @@ +.o_contributors_render { + .o_contributors_render_table { + .o_contributors_render_table_header { + &.sortable { + cursor: pointer; + } + } + } +} diff --git a/contributors_github/static/src/components/contributors_render/contributors_render.xml b/contributors_github/static/src/components/contributors_render/contributors_render.xml new file mode 100644 index 0000000..c9577f1 --- /dev/null +++ b/contributors_github/static/src/components/contributors_render/contributors_render.xml @@ -0,0 +1,197 @@ + + + + +
+

GitHub Contributors Statistics

+
+
+ +
+
+ +
+
+ + + + MTD + YTD + MAT + + +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
+ Contributor + Organization + Repository + + + Index + + + Created PRs + + + Merged PRs + + + Reviewed PRs + + + Reviews received + + + Comments + + + Comments received + +
+ + + + + + +
+
+
+ +
diff --git a/contributors_github/templates/templates.xml b/contributors_github/templates/templates.xml new file mode 100644 index 0000000..1bd925d --- /dev/null +++ b/contributors_github/templates/templates.xml @@ -0,0 +1,76 @@ + + + + + + + diff --git a/contributors_github/tests/__init__.py b/contributors_github/tests/__init__.py new file mode 100644 index 0000000..c63e32e --- /dev/null +++ b/contributors_github/tests/__init__.py @@ -0,0 +1 @@ +from . import test_github diff --git a/contributors_github/tests/test_github.py b/contributors_github/tests/test_github.py new file mode 100644 index 0000000..e057e7f --- /dev/null +++ b/contributors_github/tests/test_github.py @@ -0,0 +1,117 @@ +# 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.organization = cls.env["contributors.organization"].create( + { + "name": "oca", + "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.contributors_github.models.contributors_organization.github3" + ) as mock_github3, + patch( + "odoo.addons.contributors_github.models.contributors_organization.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.organization.update_information() + self.assertEqual( + self.organization.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.contributors_github.models.contributors_organization.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.organization.update_information() + self.assertEqual(len(self.organization.repository_ids), 2) + repo_names = {repo.name for repo in self.organization.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.organization.repository_ids.filtered( + lambda r: r.name == "server-tools" + ) + + def test_update_repository(self): + repository = self.test_update_organization_with_repository() + self.assertFalse(repository.pull_request_ids) + with patch( + "odoo.addons.contributors_github.models.contributors_repository.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.pull_request_ids) diff --git a/contributors_github/views/contributors_branch.xml b/contributors_github/views/contributors_branch.xml new file mode 100644 index 0000000..b7cb376 --- /dev/null +++ b/contributors_github/views/contributors_branch.xml @@ -0,0 +1,27 @@ + + + + + contributors.branch + +
+
+ + + + + + + + + + + contributors.branch + + + + + + + diff --git a/contributors_github/views/contributors_comment.xml b/contributors_github/views/contributors_comment.xml new file mode 100644 index 0000000..ef7ff26 --- /dev/null +++ b/contributors_github/views/contributors_comment.xml @@ -0,0 +1,26 @@ + + + + + contributors.comment + +
+
+ + + + + + + + + + contributors.comment + + + + + + + diff --git a/contributors_github/views/contributors_organization.xml b/contributors_github/views/contributors_organization.xml new file mode 100644 index 0000000..5fc9879 --- /dev/null +++ b/contributors_github/views/contributors_organization.xml @@ -0,0 +1,81 @@ + + + + + contributors.organization + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + contributors.organization + + + + + + + + + + contributors.organization + + + + + + + + + Organizations + contributors-organizations + contributors.organization + list,form + [] + {} + + + + Organizations + + + + +
diff --git a/contributors_github/views/contributors_pull_request.xml b/contributors_github/views/contributors_pull_request.xml new file mode 100644 index 0000000..7698c61 --- /dev/null +++ b/contributors_github/views/contributors_pull_request.xml @@ -0,0 +1,94 @@ + + + + + contributors.pull.request + +
+
+ + + + + + + + + + + + + + + + + + + + + + + contributors.pull.request + + + + + + + + + + + + + contributors.pull.request + + + + + + + + + + + + + + Pull Requests + contributors.pull.request + list,form + [] + {} + + + + Pull Requests + contributors-pull-requests + contributors.pull.request + list,form + [("repository_id", "=", active_id)] + {} + + + + Pull Requests + + + + + diff --git a/contributors_github/views/contributors_repository.xml b/contributors_github/views/contributors_repository.xml new file mode 100644 index 0000000..ff97d28 --- /dev/null +++ b/contributors_github/views/contributors_repository.xml @@ -0,0 +1,102 @@ + + + + + contributors.repository + +
+
+
+ +
+ + + + +
+ + + + + +
+
+
+
+ + + contributors.repository + + + + + + + + + + contributors.repository + + + + + + + + + + + + Repositories + contributors-repositories + contributors.repository + list,form + [] + {} + + + + Repositories + + + + +
diff --git a/contributors_github/views/contributors_review.xml b/contributors_github/views/contributors_review.xml new file mode 100644 index 0000000..1fe2fa5 --- /dev/null +++ b/contributors_github/views/contributors_review.xml @@ -0,0 +1,27 @@ + + + + + contributors.review + +
+
+ + + + + + + + + + + contributors.review + + + + + + + diff --git a/contributors_github/views/menu.xml b/contributors_github/views/menu.xml new file mode 100644 index 0000000..af4c2a1 --- /dev/null +++ b/contributors_github/views/menu.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/contributors_github/views/res_partner.xml b/contributors_github/views/res_partner.xml new file mode 100644 index 0000000..1a5f48d --- /dev/null +++ b/contributors_github/views/res_partner.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8fbceba --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# generated from manifests external_dependencies +PyYAML +github3.py From beccfa454a36ee977cdb87f726098157c678c62d Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 7 Jan 2026 16:34:35 +0100 Subject: [PATCH 02/44] [ADD] contributors_github_psc --- contributors_github_psc/README.rst | 65 +++ contributors_github_psc/__init__.py | 2 + contributors_github_psc/__manifest__.py | 26 ++ .../controllers/__init__.py | 1 + contributors_github_psc/controllers/main.py | 45 ++ contributors_github_psc/models/__init__.py | 4 + .../models/contributors_organization.py | 101 +++++ .../models/contributors_organization_psc.py | 21 + .../models/contributors_repository.py | 13 + contributors_github_psc/models/res_partner.py | 13 + contributors_github_psc/pyproject.toml | 3 + .../readme/CONTRIBUTORS.md | 2 + contributors_github_psc/readme/DESCRIPTION.md | 3 + .../security/ir.model.access.csv | 3 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 420 ++++++++++++++++++ .../contributors_render.esm.js | 60 +++ .../contributors_render.scss | 24 + .../contributors_render.xml | 19 + .../views/contributors_organization.xml | 23 + .../views/contributors_organization_psc.xml | 31 ++ .../views/contributors_repository.xml | 17 + 22 files changed, 896 insertions(+) create mode 100644 contributors_github_psc/README.rst create mode 100644 contributors_github_psc/__init__.py create mode 100644 contributors_github_psc/__manifest__.py create mode 100644 contributors_github_psc/controllers/__init__.py create mode 100644 contributors_github_psc/controllers/main.py create mode 100644 contributors_github_psc/models/__init__.py create mode 100644 contributors_github_psc/models/contributors_organization.py create mode 100644 contributors_github_psc/models/contributors_organization_psc.py create mode 100644 contributors_github_psc/models/contributors_repository.py create mode 100644 contributors_github_psc/models/res_partner.py create mode 100644 contributors_github_psc/pyproject.toml create mode 100644 contributors_github_psc/readme/CONTRIBUTORS.md create mode 100644 contributors_github_psc/readme/DESCRIPTION.md create mode 100644 contributors_github_psc/security/ir.model.access.csv create mode 100644 contributors_github_psc/static/description/icon.png create mode 100644 contributors_github_psc/static/description/index.html create mode 100644 contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js create mode 100644 contributors_github_psc/static/src/components/contributors_render/contributors_render.scss create mode 100644 contributors_github_psc/static/src/components/contributors_render/contributors_render.xml create mode 100644 contributors_github_psc/views/contributors_organization.xml create mode 100644 contributors_github_psc/views/contributors_organization_psc.xml create mode 100644 contributors_github_psc/views/contributors_repository.xml diff --git a/contributors_github_psc/README.rst b/contributors_github_psc/README.rst new file mode 100644 index 0000000..d7b1832 --- /dev/null +++ b/contributors_github_psc/README.rst @@ -0,0 +1,65 @@ +======================= +Contributors Github Psc +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:183c6a4f3366dc8045913f644b065e91fe8cd0cc8f869dc7b02ca87df5267e50 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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--contributors%2Fcontributors--module-lightgray.png?logo=github + :target: https://github.com/OCA-contributors/contributors-module/tree/18.0/contributors_github_psc + :alt: OCA-contributors/contributors-module + +|badge1| |badge2| |badge3| + +This module adds PSCs information to users by following a repository of +configuration that executes + +https://github.com/OCA/repo-maintainer + +**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 + +Contributors +------------ + +- Firstname Lastname email.address@example.org (optional company website + url) +- Second Person second.person@example.org (optional company website url) + +Maintainers +----------- + +This module is part of the `OCA-contributors/contributors-module `_ project on GitHub. + +You are welcome to contribute. diff --git a/contributors_github_psc/__init__.py b/contributors_github_psc/__init__.py new file mode 100644 index 0000000..91c5580 --- /dev/null +++ b/contributors_github_psc/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/contributors_github_psc/__manifest__.py b/contributors_github_psc/__manifest__.py new file mode 100644 index 0000000..9e796f8 --- /dev/null +++ b/contributors_github_psc/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Contributors Github Psc", + "summary": """Integrate PSCs""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA-contributors/contributors-module", + "depends": ["contributors_github"], + "data": [ + "views/contributors_repository.xml", + "security/ir.model.access.csv", + "views/contributors_organization_psc.xml", + "views/contributors_organization.xml", + ], + "assets": { + "web.assets_frontend": [ + "contributors_github_psc/static/src/components/**/*.esm.js", + "contributors_github_psc/static/src/components/**/*.xml", + "contributors_github_psc/static/src/components/**/*.scss", + ], + }, + "demo": [], +} diff --git a/contributors_github_psc/controllers/__init__.py b/contributors_github_psc/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/contributors_github_psc/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/contributors_github_psc/controllers/main.py b/contributors_github_psc/controllers/main.py new file mode 100644 index 0000000..01171a8 --- /dev/null +++ b/contributors_github_psc/controllers/main.py @@ -0,0 +1,45 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.contributors_github.controllers.main import ContributorsController + + +class ContributorsPSCController(ContributorsController): + def _get_merged_domain(self, organization, start, end, psc_id=None, **values): + result = super()._get_merged_domain( + organization, start, end, psc_id=psc_id, **values + ) + if psc_id: + result.append(("repository_id.psc_id", "=", int(psc_id))) + return result + + def _get_created_domain(self, organization, start, end, psc_id=None, **values): + result = super()._get_created_domain( + organization, start, end, psc_id=psc_id, **values + ) + if psc_id: + result.append(("repository_id.psc_id", "=", int(psc_id))) + return result + + def _get_comments_domain(self, organization, start, end, psc_id=None, **values): + result = super()._get_comments_domain( + organization, start, end, psc_id=psc_id, **values + ) + if psc_id: + result.append(("repository_id.psc_id", "=", int(psc_id))) + return result + + def _get_reviews_domain(self, organization, start, end, psc_id=None, **values): + result = super()._get_reviews_domain( + organization, start, end, psc_id=psc_id, **values + ) + if psc_id: + result.append(("repository_id.psc_id", "=", int(psc_id))) + return result + + def _get_partner_name(self, partner, psc_id=None, **kwargs): + result = super()._get_partner_name(partner, **kwargs) + if psc_id: + if psc_id in partner.psc_ids.ids: + result += " ★" + return result diff --git a/contributors_github_psc/models/__init__.py b/contributors_github_psc/models/__init__.py new file mode 100644 index 0000000..608b406 --- /dev/null +++ b/contributors_github_psc/models/__init__.py @@ -0,0 +1,4 @@ +from . import contributors_organization +from . import contributors_organization_psc +from . import contributors_repository +from . import res_partner diff --git a/contributors_github_psc/models/contributors_organization.py b/contributors_github_psc/models/contributors_organization.py new file mode 100644 index 0000000..b649fbb --- /dev/null +++ b/contributors_github_psc/models/contributors_organization.py @@ -0,0 +1,101 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import requests +import yaml + +from odoo import fields, models + + +class ContributorsOrganization(models.Model): + _inherit = "contributors.organization" + + psc_repository = fields.Char() + psc_repository_conf = fields.Char() + psc_ids = fields.One2many( + "contributors.organization.psc", + inverse_name="organization_id", + readonly=True, + ) + + def update_information(self): + res = super().update_information() + if self.psc_repository: + client = self._get_clients()[0] + repository = client.repository(self.name, self.psc_repository) + psc_files = repository.directory_contents(f"{self.psc_repository_conf}/psc") + repository_files = repository.directory_contents( + f"{self.psc_repository_conf}/repo" + ) + for _psc_filename, psc_file in psc_files: + req = requests.get(psc_file.download_url, timeout=10) + req.raise_for_status() + psc_data = yaml.safe_load(req.content) + for psc_name in psc_data: + psc_record = self.env["contributors.organization.psc"].search( + [ + ("organization_id", "=", self.id), + ("key", "=", psc_name), + ], + limit=1, + ) + if not psc_record: + psc_record = ( + self.env["contributors.organization.psc"] + .sudo() + .create( + { + "organization_id": self.id, + "key": psc_name, + "name": psc_data[psc_name].get("name", psc_name), + } + ) + ) + else: + psc_record.sudo().write( + { + "name": psc_data[psc_name].get("name", psc_name), + } + ) + member_logins = psc_data[psc_name].get("members", []) + psc_data[ + psc_name + ].get("representatives", []) + members = [] + for member in member_logins: + members.append( + self.env["res.partner"]._get_github_user(member, client) + ) + psc_record.member_ids = self.env["res.partner"].browse(members) + for _repo_filename, repo_file in repository_files: + req = requests.get(repo_file.download_url, timeout=10) + req.raise_for_status() + repo_data = yaml.safe_load(req.content) + for repo_name in repo_data: + psc_name = repo_data[repo_name].get("psc") + psc_record = self.env["contributors.organization.psc"].search( + [ + ("organization_id", "=", self.id), + ("key", "=", psc_name), + ], + limit=1, + ) + if not psc_record: + continue + repo_record = ( + self.env["contributors.repository"] + .sudo() + .search( + [ + ("organization_id", "=", self.id), + ("name", "=", repo_name), + ], + limit=1, + ) + ) + if repo_record: + repo_record.sudo().write( + { + "psc_id": psc_record.id, + } + ) + return res diff --git a/contributors_github_psc/models/contributors_organization_psc.py b/contributors_github_psc/models/contributors_organization_psc.py new file mode 100644 index 0000000..20446d4 --- /dev/null +++ b/contributors_github_psc/models/contributors_organization_psc.py @@ -0,0 +1,21 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ContributorsOrganizationPsc(models.Model): + _name = "contributors.organization.psc" + _description = "Contributors Organization Psc" # TODO + + organization_id = fields.Many2one( + comodel_name="contributors.organization", + string="Organization", + required=True, + ) + name = fields.Char(required=True) + key = fields.Char(required=True) + member_ids = fields.Many2many( + "res.partner", + readonly=True, + ) diff --git a/contributors_github_psc/models/contributors_repository.py b/contributors_github_psc/models/contributors_repository.py new file mode 100644 index 0000000..955a593 --- /dev/null +++ b/contributors_github_psc/models/contributors_repository.py @@ -0,0 +1,13 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ContributorsRepository(models.Model): + _inherit = "contributors.repository" + + psc_id = fields.Many2one( + "contributors.organization.psc", + readonly=True, + ) diff --git a/contributors_github_psc/models/res_partner.py b/contributors_github_psc/models/res_partner.py new file mode 100644 index 0000000..49fb88f --- /dev/null +++ b/contributors_github_psc/models/res_partner.py @@ -0,0 +1,13 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + psc_ids = fields.Many2many( + comodel_name="contributors.organization.psc", + readonly=True, + ) diff --git a/contributors_github_psc/pyproject.toml b/contributors_github_psc/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/contributors_github_psc/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/contributors_github_psc/readme/CONTRIBUTORS.md b/contributors_github_psc/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..7be72fb --- /dev/null +++ b/contributors_github_psc/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Firstname Lastname (optional company website url) +- Second Person (optional company website url) diff --git a/contributors_github_psc/readme/DESCRIPTION.md b/contributors_github_psc/readme/DESCRIPTION.md new file mode 100644 index 0000000..026a305 --- /dev/null +++ b/contributors_github_psc/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module adds PSCs information to users by following a repository of configuration that executes + +https://github.com/OCA/repo-maintainer diff --git a/contributors_github_psc/security/ir.model.access.csv b/contributors_github_psc/security/ir.model.access.csv new file mode 100644 index 0000000..b3d9c91 --- /dev/null +++ b/contributors_github_psc/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_organization_psc,Access Organization PSC,model_contributors_organization_psc,base.group_user,1,0,0,0 +access_organization_portal_psc,Access Organization PSC,model_contributors_organization_psc,base.group_portal,1,0,0,0 diff --git a/contributors_github_psc/static/description/icon.png b/contributors_github_psc/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/contributors_github_psc/static/description/index.html b/contributors_github_psc/static/description/index.html new file mode 100644 index 0000000..1692f8b --- /dev/null +++ b/contributors_github_psc/static/description/index.html @@ -0,0 +1,420 @@ + + + + + +Contributors Github Psc + + + +
+

Contributors Github Psc

+ + +

Beta License: AGPL-3 OCA-contributors/contributors-module

+

This module adds PSCs information to users by following a repository of +configuration that executes

+

https://github.com/OCA/repo-maintainer

+

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
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the OCA-contributors/contributors-module project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js b/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js new file mode 100644 index 0000000..051fbc5 --- /dev/null +++ b/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js @@ -0,0 +1,60 @@ +import {AutoComplete} from "@web/core/autocomplete/autocomplete"; +import {ContributorsRender} from "@contributors_github/components/contributors_render/contributors_render.esm"; +import {onMounted} from "@odoo/owl"; +import {patch} from "@web/core/utils/patch"; +import {useService} from "@web/core/utils/hooks"; + +patch(ContributorsRender, { + components: { + ...ContributorsRender.components, + AutoComplete, + }, +}); +patch(ContributorsRender.prototype, { + setup() { + super.setup(); + this.state.psc = ""; + this.state.pscs = []; + this.state.pscId = null; + this.orm = useService("orm"); + onMounted(this.fetchPscs.bind(this)); + }, + async fetchPscs() { + const pscs = await this.orm.searchRead( + "contributors.organization.psc", + [["organization_id", "=", this.props.organization]], + ["id", "name", "member_ids"] + ); + this.state.rawPscs = Object.fromEntries(pscs.map((psc) => [psc.id, psc])); + this.state.pscs = pscs.map((psc) => { + return { + label: psc.name, + value: psc.id, + }; + }); + }, + get pscSources() { + const pscs = this.state.pscs; + return [ + { + async options(query) { + return pscs.filter((psc) => + psc.label.toLowerCase().includes(query.toLowerCase()) + ); + }, + }, + ]; + }, + onSelectPsc(option) { + this.state.pscId = option.value; + this.state.psc = this.state.rawPscs[option.value].name; + this.fetchData(); + }, + getParameters() { + const params = super.getParameters(); + if (this.state.pscId) { + params.psc_id = this.state.pscId; + } + return params; + }, +}); diff --git a/contributors_github_psc/static/src/components/contributors_render/contributors_render.scss b/contributors_github_psc/static/src/components/contributors_render/contributors_render.scss new file mode 100644 index 0000000..240958a --- /dev/null +++ b/contributors_github_psc/static/src/components/contributors_render/contributors_render.scss @@ -0,0 +1,24 @@ +.o_contributors_render { + .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/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml b/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml new file mode 100644 index 0000000..5a9b9f8 --- /dev/null +++ b/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml @@ -0,0 +1,19 @@ + + + + + +
+ +
+
+
+ +
diff --git a/contributors_github_psc/views/contributors_organization.xml b/contributors_github_psc/views/contributors_organization.xml new file mode 100644 index 0000000..edf61cf --- /dev/null +++ b/contributors_github_psc/views/contributors_organization.xml @@ -0,0 +1,23 @@ + + + + + contributors.organization + + + + + + + + + + + + + + diff --git a/contributors_github_psc/views/contributors_organization_psc.xml b/contributors_github_psc/views/contributors_organization_psc.xml new file mode 100644 index 0000000..d69f97f --- /dev/null +++ b/contributors_github_psc/views/contributors_organization_psc.xml @@ -0,0 +1,31 @@ + + + + + contributors.organization.psc + +
+
+ + + + + + + + + + + + + contributors.organization.psc + + + + + + + + + diff --git a/contributors_github_psc/views/contributors_repository.xml b/contributors_github_psc/views/contributors_repository.xml new file mode 100644 index 0000000..8f2287e --- /dev/null +++ b/contributors_github_psc/views/contributors_repository.xml @@ -0,0 +1,17 @@ + + + + + contributors.repository + + + + + + + + From 0e5c31ce98205b971f7bc13a2c75173265e1ed95 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Fri, 9 Jan 2026 14:24:22 +0100 Subject: [PATCH 03/44] [FIX] contributors_github: Set the right github_name --- contributors_github/models/res_partner.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contributors_github/models/res_partner.py b/contributors_github/models/res_partner.py index a14da2b..4c87e4e 100644 --- a/contributors_github/models/res_partner.py +++ b/contributors_github/models/res_partner.py @@ -36,16 +36,17 @@ class ResPartner(models.Model): def _get_github_user(self, gh, client): if not gh: return False + github_login = str(gh) partner = self.with_context(active_test=False).search( - [("github_name", "=ilike", str(gh))], limit=1 + [("github_name", "=ilike", github_login)], limit=1 ) if not partner: if isinstance(gh, str): - gh = client.user(str(gh)) + gh = client.user(github_login) return self.create( { "name": getattr(gh, "name", None) or str(gh), - "github_name": str(gh), + "github_name": github_login, "github_user": True, } ).id From 944ba67d673fc4b8902306086f9612bb27a9e7a7 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 10 Jan 2026 15:36:19 +0100 Subject: [PATCH 04/44] [FIX] contributors_github --- contributors_github/controllers/main.py | 148 ++++++++++++++++-- contributors_github/data/ir_cron.xml | 9 ++ .../models/contributors_organization.py | 12 ++ contributors_github/models/res_partner.py | 10 +- .../contributors_render.esm.js | 19 +-- .../contributors_render.scss | 22 +++ .../contributors_render.xml | 146 +++++------------ .../views/contributors_organization.xml | 1 + 8 files changed, 225 insertions(+), 142 deletions(-) diff --git a/contributors_github/controllers/main.py b/contributors_github/controllers/main.py index 8386dac..4e523c9 100644 --- a/contributors_github/controllers/main.py +++ b/contributors_github/controllers/main.py @@ -7,7 +7,7 @@ from dateutil.relativedelta import relativedelta -from odoo import http +from odoo import _, http from odoo.http import request from odoo.addons.portal.controllers.portal import CustomerPortal @@ -94,6 +94,15 @@ def _get_index(self, data): 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(["/contributors/fetch"], type="json", auth="user", readonly=True) def fetch_data(self, organization_id, year, month, kind, period, **values): start, end = self._get_dates(year, month, period, **values) @@ -102,26 +111,131 @@ def fetch_data(self, organization_id, year, month, kind, period, **values): ) if not organization: return [] - data = defaultdict( - lambda: { - "name": "", - "github_name": "", - "index": 0, - "created_pull_requests": 0, - "merged_pull_requests": 0, - "comments": 0, - "reviews": 0, - } + data = self._generate_data( + organization, start, end, self._get_field(kind), kind, **values ) + return { + "columns": self._get_columns(kind), + "data": self._improve_data(data, kind, **values), + } + + def _get_columns(self, kind): if kind == "contributors": - field = "partner_id" + return [ + {"field": "name", "title": _("Name"), "kind": "name"}, + { + "field": "index", + "title": _("Contributor Index"), + "kind": "float", + "decimals": 2, + }, + { + "field": "created_pull_requests", + "title": _("Created Pull Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "merged_pull_requests", + "title": _("Merged Pull Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "comments", + "title": _("Comments"), + "kind": "float", + "decimals": 0, + }, + { + "field": "reviews", + "title": _("Reviews"), + "kind": "float", + "decimals": 0, + }, + ] elif kind == "organizations": - field = "organization_id" + return [ + {"field": "name", "title": _("Organization Name"), "kind": "name"}, + { + "field": "created_pull_requests", + "title": _("Created Pull Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "merged_pull_requests", + "title": _("Merged Pull Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "comments", + "title": _("Comments"), + "kind": "float", + "decimals": 0, + }, + { + "field": "reviews", + "title": _("Reviews"), + "kind": "float", + "decimals": 0, + }, + ] elif kind == "repositories": - field = "repository_id" - else: - return [] + return [ + {"field": "name", "title": _("Repository Name"), "kind": "name"}, + { + "field": "index", + "title": _("Repository Index"), + "kind": "float", + "decimals": 2, + }, + { + "field": "created_pull_requests", + "title": _("Created Pull Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "merged_pull_requests", + "title": _("Merged Pull Requests"), + "kind": "float", + "decimals": 0, + }, + { + "field": "comments", + "title": _("Comments"), + "kind": "float", + "decimals": 0, + }, + { + "field": "reviews", + "title": _("Reviews"), + "kind": "float", + "decimals": 0, + }, + ] + return [] + + def _get_default_data(self, organization, start, end, field, kind, **values): + return { + "name": "", + "github_name": "", + "index": 0, + "created_pull_requests": 0, + "merged_pull_requests": 0, + "comments": 0, + "reviews": 0, + } + def _generate_data(self, organization, start, end, field, kind, **values): + default_dict = self._get_default_data( + organization, start, end, field, kind, **values + ) + data = defaultdict(lambda: default_dict.copy()) + if not field: + return data for merged in ( request.env["contributors.pull.request"] .sudo() @@ -166,7 +280,7 @@ def fetch_data(self, organization_id, year, month, kind, period, **values): ) ): data[review[field][0]]["reviews"] = review[f"{field}_count"] - return self._improve_data(data, kind, **values) + return data def _improve_data(self, data, kind, **kwargs): for key, values in data.items(): diff --git a/contributors_github/data/ir_cron.xml b/contributors_github/data/ir_cron.xml index 5a758c2..5420b5a 100644 --- a/contributors_github/data/ir_cron.xml +++ b/contributors_github/data/ir_cron.xml @@ -11,4 +11,13 @@ minutes False + + GitHub Organization Update + + code + model._cron_update_organizations() + 1 + days + False + diff --git a/contributors_github/models/contributors_organization.py b/contributors_github/models/contributors_organization.py index 3dda1eb..56f06a1 100644 --- a/contributors_github/models/contributors_organization.py +++ b/contributors_github/models/contributors_organization.py @@ -1,12 +1,15 @@ # Copyright 2026 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import base64 +import logging import github3 import requests from odoo import fields, models +_logger = logging.getLogger(__name__) + class ContributorsOrganization(models.Model): _name = "contributors.organization" @@ -68,6 +71,15 @@ def update_information(self): self.env["contributors.repository"]._update_repository(repo, self) self.last_update = fields.Datetime.now() + def _cron_update_organizations(self): + for organization in self.search([("key_ids", "!=", False)]): + try: + organization.update_information() + except Exception as e: + _logger.error( + "Error updating organization %s: %s", organization.name, str(e) + ) + class ContributorsOrganizationKey(models.Model): _name = "contributors.organization.key" diff --git a/contributors_github/models/res_partner.py b/contributors_github/models/res_partner.py index 4c87e4e..fd0a039 100644 --- a/contributors_github/models/res_partner.py +++ b/contributors_github/models/res_partner.py @@ -42,10 +42,16 @@ def _get_github_user(self, gh, client): ) if not partner: if isinstance(gh, str): - gh = client.user(github_login) + try: + gh = client.user(github_login) + name = gh.name or github_login + except github3.exceptions.NotFoundError: + name = gh + else: + name = gh.name or github_login return self.create( { - "name": getattr(gh, "name", None) or str(gh), + "name": name, "github_name": github_login, "github_user": True, } diff --git a/contributors_github/static/src/components/contributors_render/contributors_render.esm.js b/contributors_github/static/src/components/contributors_render/contributors_render.esm.js index 073e7f4..a13e456 100644 --- a/contributors_github/static/src/components/contributors_render/contributors_render.esm.js +++ b/contributors_github/static/src/components/contributors_render/contributors_render.esm.js @@ -22,6 +22,7 @@ export class ContributorsRender extends Component { organizations: "merged_pull_requests", repositories: "merged_pull_requests", }, + columns: {}, period: "YTD", contributors: [], organizations: [], @@ -37,22 +38,16 @@ export class ContributorsRender extends Component { this.fetchData(); } async fetchData() { + const data = await rpc("/contributors/fetch", this.getParameters()); if (this.state.kind === "contributors") { - this.state.contributors = await rpc( - "/contributors/fetch", - this.getParameters() - ); + this.state.contributors = data.data; } else if (this.state.kind === "organizations") { - this.state.organizations = await rpc( - "/contributors/fetch", - this.getParameters() - ); + this.state.organizations = data.data; } else if (this.state.kind === "repositories") { - this.state.repositories = await rpc( - "/contributors/fetch", - this.getParameters() - ); + this.state.repositories = data.data; } + this.state.columns = data.columns; + return data; } getParameters() { return { diff --git a/contributors_github/static/src/components/contributors_render/contributors_render.scss b/contributors_github/static/src/components/contributors_render/contributors_render.scss index 02ec11a..75ce0ce 100644 --- a/contributors_github/static/src/components/contributors_render/contributors_render.scss +++ b/contributors_github/static/src/components/contributors_render/contributors_render.scss @@ -6,4 +6,26 @@ } } } + .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/contributors_github/static/src/components/contributors_render/contributors_render.xml b/contributors_github/static/src/components/contributors_render/contributors_render.xml index c9577f1..989a60a 100644 --- a/contributors_github/static/src/components/contributors_render/contributors_render.xml +++ b/contributors_github/static/src/components/contributors_render/contributors_render.xml @@ -3,7 +3,7 @@
-

GitHub Contributors Statistics

+

Contributors Statistics

-
+
-
+
Date: Sat, 10 Jan 2026 15:36:35 +0100 Subject: [PATCH 05/44] [FIX] contributors_github_psc --- contributors_github_psc/__manifest__.py | 1 - .../contributors_render.esm.js | 2 ++ .../contributors_render.scss | 24 ------------------- .../contributors_render.xml | 1 + 4 files changed, 3 insertions(+), 25 deletions(-) delete mode 100644 contributors_github_psc/static/src/components/contributors_render/contributors_render.scss diff --git a/contributors_github_psc/__manifest__.py b/contributors_github_psc/__manifest__.py index 9e796f8..a2b89b9 100644 --- a/contributors_github_psc/__manifest__.py +++ b/contributors_github_psc/__manifest__.py @@ -19,7 +19,6 @@ "web.assets_frontend": [ "contributors_github_psc/static/src/components/**/*.esm.js", "contributors_github_psc/static/src/components/**/*.xml", - "contributors_github_psc/static/src/components/**/*.scss", ], }, "demo": [], diff --git a/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js b/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js index 051fbc5..dc7de60 100644 --- a/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js +++ b/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js @@ -1,5 +1,6 @@ import {AutoComplete} from "@web/core/autocomplete/autocomplete"; import {ContributorsRender} from "@contributors_github/components/contributors_render/contributors_render.esm"; +import {_t} from "@web/core/l10n/translation"; import {onMounted} from "@odoo/owl"; import {patch} from "@web/core/utils/patch"; import {useService} from "@web/core/utils/hooks"; @@ -17,6 +18,7 @@ patch(ContributorsRender.prototype, { this.state.pscs = []; this.state.pscId = null; this.orm = useService("orm"); + this.selectPsc = _t("Select PSC"); onMounted(this.fetchPscs.bind(this)); }, async fetchPscs() { diff --git a/contributors_github_psc/static/src/components/contributors_render/contributors_render.scss b/contributors_github_psc/static/src/components/contributors_render/contributors_render.scss deleted file mode 100644 index 240958a..0000000 --- a/contributors_github_psc/static/src/components/contributors_render/contributors_render.scss +++ /dev/null @@ -1,24 +0,0 @@ -.o_contributors_render { - .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/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml b/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml index 5a9b9f8..17fbecb 100644 --- a/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml +++ b/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml @@ -11,6 +11,7 @@ class="" resetOnSelect="true" autoSelect="true" + placeholder="selectPsc" />
From 092b2410c6c951dfda4f3be363f1283dad4bbf58 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 10 Jan 2026 15:40:54 +0100 Subject: [PATCH 06/44] [ADD] contributors_weblate --- contributors_weblate/README.rst | 62 +++ contributors_weblate/__init__.py | 2 + contributors_weblate/__manifest__.py | 28 ++ contributors_weblate/controllers/__init__.py | 1 + contributors_weblate/controllers/main.py | 98 ++++ contributors_weblate/data/ir_cron.xml | 14 + contributors_weblate/models/__init__.py | 3 + .../models/contributors_organization.py | 178 ++++++++ .../models/contributors_repository.py | 10 + .../models/contributors_translation.py | 99 +++++ contributors_weblate/pyproject.toml | 3 + contributors_weblate/readme/CONTRIBUTORS.md | 2 + contributors_weblate/readme/DESCRIPTION.md | 1 + .../security/ir.model.access.csv | 2 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 419 ++++++++++++++++++ .../contributors_render.esm.js | 88 ++++ .../contributors_render.xml | 28 ++ .../views/contributors_organization.xml | 43 ++ .../views/contributors_repository.xml | 24 + .../views/contributors_translation.xml | 90 ++++ 21 files changed, 1195 insertions(+) create mode 100644 contributors_weblate/README.rst create mode 100644 contributors_weblate/__init__.py create mode 100644 contributors_weblate/__manifest__.py create mode 100644 contributors_weblate/controllers/__init__.py create mode 100644 contributors_weblate/controllers/main.py create mode 100644 contributors_weblate/data/ir_cron.xml create mode 100644 contributors_weblate/models/__init__.py create mode 100644 contributors_weblate/models/contributors_organization.py create mode 100644 contributors_weblate/models/contributors_repository.py create mode 100644 contributors_weblate/models/contributors_translation.py create mode 100644 contributors_weblate/pyproject.toml create mode 100644 contributors_weblate/readme/CONTRIBUTORS.md create mode 100644 contributors_weblate/readme/DESCRIPTION.md create mode 100644 contributors_weblate/security/ir.model.access.csv create mode 100644 contributors_weblate/static/description/icon.png create mode 100644 contributors_weblate/static/description/index.html create mode 100644 contributors_weblate/static/src/components/contributors_render/contributors_render.esm.js create mode 100644 contributors_weblate/static/src/components/contributors_render/contributors_render.xml create mode 100644 contributors_weblate/views/contributors_organization.xml create mode 100644 contributors_weblate/views/contributors_repository.xml create mode 100644 contributors_weblate/views/contributors_translation.xml diff --git a/contributors_weblate/README.rst b/contributors_weblate/README.rst new file mode 100644 index 0000000..665d800 --- /dev/null +++ b/contributors_weblate/README.rst @@ -0,0 +1,62 @@ +==================== +Contributors Weblate +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:625192af696f9a4fab610449af3ab38f84397ea8822a8986bbbee7aac43b23f1 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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--contributors%2Fcontributors--module-lightgray.png?logo=github + :target: https://github.com/OCA-contributors/contributors-module/tree/18.0/contributors_weblate + :alt: OCA-contributors/contributors-module + +|badge1| |badge2| |badge3| + +Get Weblate data and integrate it inside your contributors information. + +**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 + +Contributors +------------ + +- `Dixmit `__ + + - Enric Tobella + +Maintainers +----------- + +This module is part of the `OCA-contributors/contributors-module `_ project on GitHub. + +You are welcome to contribute. diff --git a/contributors_weblate/__init__.py b/contributors_weblate/__init__.py new file mode 100644 index 0000000..f7209b1 --- /dev/null +++ b/contributors_weblate/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/contributors_weblate/__manifest__.py b/contributors_weblate/__manifest__.py new file mode 100644 index 0000000..70faaa0 --- /dev/null +++ b/contributors_weblate/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Contributors Weblate", + "summary": """Get Weblate information and integrate it in the system""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA-contributors/contributors-module", + "depends": [ + "contributors_github", + ], + "data": [ + "security/ir.model.access.csv", + "data/ir_cron.xml", + "views/contributors_translation.xml", + "views/contributors_repository.xml", + "views/contributors_organization.xml", + ], + "demo": [], + "assets": { + "web.assets_frontend": [ + "contributors_weblate/static/src/**/*.xml", + "contributors_weblate/static/src/**/*.esm.js", + ], + }, +} diff --git a/contributors_weblate/controllers/__init__.py b/contributors_weblate/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/contributors_weblate/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/contributors_weblate/controllers/main.py b/contributors_weblate/controllers/main.py new file mode 100644 index 0000000..b02cd4a --- /dev/null +++ b/contributors_weblate/controllers/main.py @@ -0,0 +1,98 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import _ +from odoo.http import request + +from odoo.addons.contributors_github.controllers.main import ContributorsController + + +class ContributorsPSCController(ContributorsController): + def _get_columns(self, kind): + columns = super()._get_columns(kind) + if kind == "translations": + columns = [ + {"field": "name", "title": _("Name"), "kind": "name"}, + ] + columns + if kind in ["contributors", "repositories", "translations"]: + columns.append( + { + "field": "translations", + "title": _("Translations"), + "kind": "float", + "decimals": 0, + } + ) + return columns + + def _get_default_data(self, organization, start, end, field, kind, **values): + data = super()._get_default_data( + organization, start, end, field, kind, **values + ) + data["translations"] = 0 + return data + + def _translation_actions(self): + return ["5", "8", "25", "27"] + + def _get_translation_domain( + self, + organization, + start, + end, + psc_id=None, + lang_id=None, + actions=None, + **values, + ): + if actions is None: + actions = self._translation_actions() + domain = [ + ("organization_id", "=", organization.id), + ("date", ">=", start), + ("date", "<", end), + ("action", "in", actions), + ] + if lang_id: + domain.append(("lang_id", "=", lang_id)) + if psc_id and "psc_id" in request.env["contributors.repository"]._fields: + # To avoid an extra module we add the psc_id filter here + domain.append(("repository_id.psc_id", "=", int(psc_id))) + return domain + + def _improve_data(self, data, kind, **kwargs): + data = super()._improve_data(data, kind, **kwargs) + if kind == "translations": + for key, values in data.items(): + partner = request.env["res.partner"].browse(key) + values["name"] = self._get_partner_name(partner, **kwargs) + values["github_name"] = partner.github_name + return data + + def _generate_data( + self, organization, start, end, field, kind, lang_id=None, **values + ): + data = super()._generate_data(organization, start, end, field, kind, **values) + if kind == "translations" and not field: + field = "partner_id" + if ( + kind in ["contributors", "repositories", "translations"] + and organization.weblate_url + ): + for merged in ( + request.env["contributors.translation"] + .sudo() + .read_group( + self._get_translation_domain( + organization, + start, + end, + lang_id=lang_id if kind == "translations" else None, + **values, + ) + + [(field, "!=", False)], + [field], + [field], + ) + ): + data[merged[field][0]]["translations"] = merged[f"{field}_count"] + return data diff --git a/contributors_weblate/data/ir_cron.xml b/contributors_weblate/data/ir_cron.xml new file mode 100644 index 0000000..6ea474b --- /dev/null +++ b/contributors_weblate/data/ir_cron.xml @@ -0,0 +1,14 @@ + + + + + Weblate Organization Update + + code + model._cron_weblate_data() + 1 + minutes + False + + diff --git a/contributors_weblate/models/__init__.py b/contributors_weblate/models/__init__.py new file mode 100644 index 0000000..256ed35 --- /dev/null +++ b/contributors_weblate/models/__init__.py @@ -0,0 +1,3 @@ +from . import contributors_organization +from . import contributors_translation +from . import contributors_repository diff --git a/contributors_weblate/models/contributors_organization.py b/contributors_weblate/models/contributors_organization.py new file mode 100644 index 0000000..d6fb894 --- /dev/null +++ b/contributors_weblate/models/contributors_organization.py @@ -0,0 +1,178 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging +import re +from datetime import timedelta + +import requests + +from odoo import _, fields, models, tools +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class ContributorsOrganization(models.Model): + _inherit = "contributors.organization" + + weblate_url = fields.Char() + weblate_auth_token = fields.Char() + weblate_last_update = fields.Datetime() + weblate_load_delay = fields.Integer( + help="Number of days to load each time", default=7 + ) + + def get_weblate_data(self): + self.ensure_one() + if not self.weblate_last_update: + raise ValidationError( + _("Please set the last update date for Weblate data.") + ) + auth = {} + if self.weblate_auth_token: + auth = {"Authorization": f"Token {self.weblate_auth_token}"} + before_date = min( + self.weblate_last_update + timedelta(days=self.weblate_load_delay), + fields.Datetime.now(), + ) + req = requests.get( + f"{self.weblate_url}/api/changes/", + params={ + "timestamp_after": ( + self.weblate_last_update + timedelta(days=-1) + ).isoformat(), + "timestamp_before": before_date.isoformat(), + "format": "json", + }, + timeout=10, + headers=auth, + ) + # self.weblate_last_update = before_date + total = 0 + client = self._get_clients()[0] + while True: + results = req.json().get("results", []) + max_total = req.json().get("count", 0) + for result in results: + translation = self.env["contributors.translation"].search( + [("integration_id", "=", str(result["id"]))] + ) + partner = False + if result["user"]: + splitted = result["user"].split("/") + if len(splitted) >= 2: + partner = self.env["res.partner"]._get_github_user( + splitted[-2], client + ) + vals = { + "integration_id": result["id"], + "unit": result["unit"], + "translation": result["translation"], + "component": result["component"], + "organization_id": self.id, + "partner_id": partner, + "date": self.env["contributors.repository"]._parse_date( + result["timestamp"] + ), + "action": str(result["action"]), + "repository_id": self._get_repository(result["component"]), + "branch_id": self._get_branch(result["component"]), + "lang_id": self._get_lang(result["translation"]), + } + if not translation: + translation = ( + self.env["contributors.translation"].sudo().create(vals) + ) + else: + translation.sudo().write(vals) + total += 1 + if total % 100 == 0: + _logger.info( + f"Processed {total} of {max_total} Weblate translation changes" + ) + if req.json().get("next"): + req = requests.get(req.json().get("next"), timeout=10, headers=auth) + else: + break + self.weblate_last_update = before_date + _logger.info(f"Processed a total of {total} Weblate translation changes") + + @tools.ormcache("self.id", "translation") + def _get_lang(self, translation): + if not translation: + return False + parts = translation.split("/") + lang = ( + self.env["res.lang"] + .search( + [("iso_code", "=", parts[-2]), ("active", "in", [True, False])], limit=1 + ) + .id + ) + if lang: + return lang + if len(parts[-2]) == 2: + lang = ( + self.env["res.lang"] + .search( + [ + ("iso_code", "=ilike", parts[-2] + "_%"), + ("active", "in", [True, False]), + ], + limit=1, + ) + .id + ) + if lang: + return lang + return ( + self.env["res.lang"] + .search( + [("code", "=", parts[-2]), ("active", "in", [True, False])], limit=1 + ) + .id + ) + + @tools.ormcache("self.id", "component") + def _get_branch(self, component): + if not component: + return False + parts = component.split("/") + if len(parts) < 3: + return False + found = re.match(r"^[\w-]+-(\d+-\d)$", parts[-3]) + if not found: + return False + return self.env["contributors.branch"]._get_branch( + self, found.group(1).replace("-", ".") + ) + + @tools.ormcache("self.id", "component") + def _get_repository(self, component): + if not component: + return False + parts = component.split("/") + if len(parts) < 3: + return False + found = re.match(r"^([\w-]+)-\d+-\d$", parts[-3]) + if not found: + return False + return ( + self.env["contributors.repository"] + .search( + [("name", "=", found.group(1)), ("organization_id", "=", self.id)], + limit=1, + ) + .id + ) + + def _cron_weblate_data(self): + organizations = self.search([("weblate_url", "!=", False)]) + for organization in organizations: + try: + organization.get_weblate_data() + except Exception as e: + _logger.error( + "Error while updating Weblate data for organization " + f"{organization.name}: {e}" + ) diff --git a/contributors_weblate/models/contributors_repository.py b/contributors_weblate/models/contributors_repository.py new file mode 100644 index 0000000..c333b70 --- /dev/null +++ b/contributors_weblate/models/contributors_repository.py @@ -0,0 +1,10 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ContributorsRepository(models.Model): + _inherit = "contributors.repository" + + weblate_url = fields.Char(related="organization_id.weblate_url") diff --git a/contributors_weblate/models/contributors_translation.py b/contributors_weblate/models/contributors_translation.py new file mode 100644 index 0000000..8ef0b7a --- /dev/null +++ b/contributors_weblate/models/contributors_translation.py @@ -0,0 +1,99 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ContributorsTranslation(models.Model): + _name = "contributors.translation" + _description = "Contributors Translation" # TODO + + integration_id = fields.Char() + unit = fields.Char() + translation = fields.Char() + component = fields.Char() + partner_id = fields.Many2one("res.partner") + organization_id = fields.Many2one("contributors.organization") + repository_id = fields.Many2one("contributors.repository") + branch_id = fields.Many2one("contributors.branch") + date = fields.Datetime() + lang_id = fields.Many2one("res.lang") + action = fields.Selection( + [ + ("0", "Resource updated"), + ("1", "Translation completed"), + ("2", "Translation changed"), + ("3", "Comment added"), + ("4", "Suggestion added"), + ("5", "Translation added"), + ("6", "Automatically translated"), + ("7", "Suggestion accepted"), + ("8", "Translation reverted"), + ("9", "Translation uploaded"), + ("13", "Source string added"), + ("14", "Component locked"), + ("15", "Component unlocked"), + ("17", "Changes committed"), + ("18", "Changes pushed"), + ("19", "Repository reset"), + ("20", "Repository merged"), + ("21", "Repository rebased"), + ("22", "Repository merge failed"), + ("23", "Repository rebase failed"), + ("24", "Parsing failed"), + ("25", "Translation removed"), + ("26", "Suggestion removed"), + ("27", "Translation replaced"), + ("28", "Repository push failed"), + ("29", "Suggestion removed during cleanup"), + ("30", "Source string changed"), + ("31", "String added"), + ("32", "Bulk status changed"), + ("33", "Visibility changed"), + ("34", "User added"), + ("35", "User removed"), + ("36", "Translation approved"), + ("37", "Marked for edit"), + ("38", "Component removed"), + ("39", "Project removed"), + ("41", "Project renamed"), + ("42", "Component renamed"), + ("43", "Moved component"), + ("45", "Contributor joined"), + ("46", "Announcement posted"), + ("47", "Alert triggered"), + ("48", "Language added"), + ("49", "Language requested"), + ("50", "Project created"), + ("51", "Component created"), + ("52", "User invited"), + ("53", "Repository notification received"), + ("54", "Translation replaced file by upload"), + ("55", "License changed"), + ("56", "Contributor license agreement changed"), + ("57", "Screenshot added"), + ("58", "Screenshot uploaded"), + ("59", "String updated in the repository"), + ("60", "Add-on installed"), + ("61", "Add-on configuration changed"), + ("62", "Add-on uninstalled"), + ("63", "String removed"), + ("64", "Comment removed"), + ("65", "Comment resolved"), + ("66", "Explanation updated"), + ("67", "Category removed"), + ("68", "Category renamed"), + ("69", "Category moved"), + ("70", "Saving string failed"), + ("71", "String added in the repository"), + ("72", "String updated in the upload"), + ("73", "String added in the upload"), + ("74", "Translation updated by source upload"), + ("75", "Component translation completed"), + ("76", "Applied enforced check"), + ("77", "Propagated change"), + ("78", "File uploaded"), + ("79", "Extra flags updated"), + ] + ) + details = fields.Json() diff --git a/contributors_weblate/pyproject.toml b/contributors_weblate/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/contributors_weblate/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/contributors_weblate/readme/CONTRIBUTORS.md b/contributors_weblate/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..2c066ba --- /dev/null +++ b/contributors_weblate/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Dixmit](https://www.dixmit.com) + - Enric Tobella diff --git a/contributors_weblate/readme/DESCRIPTION.md b/contributors_weblate/readme/DESCRIPTION.md new file mode 100644 index 0000000..fca9cea --- /dev/null +++ b/contributors_weblate/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Get Weblate data and integrate it inside your contributors information. diff --git a/contributors_weblate/security/ir.model.access.csv b/contributors_weblate/security/ir.model.access.csv new file mode 100644 index 0000000..39fa2dc --- /dev/null +++ b/contributors_weblate/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_translations,Access Translation,model_contributors_translation,base.group_user,1,0,0,0 diff --git a/contributors_weblate/static/description/icon.png b/contributors_weblate/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/contributors_weblate/static/description/index.html b/contributors_weblate/static/description/index.html new file mode 100644 index 0000000..076ef5a --- /dev/null +++ b/contributors_weblate/static/description/index.html @@ -0,0 +1,419 @@ + + + + + +Contributors Weblate + + + +
+

Contributors Weblate

+ + +

Beta License: AGPL-3 OCA-contributors/contributors-module

+

Get Weblate data and integrate it inside your contributors information.

+

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
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the OCA-contributors/contributors-module project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/contributors_weblate/static/src/components/contributors_render/contributors_render.esm.js b/contributors_weblate/static/src/components/contributors_render/contributors_render.esm.js new file mode 100644 index 0000000..ba11ec6 --- /dev/null +++ b/contributors_weblate/static/src/components/contributors_render/contributors_render.esm.js @@ -0,0 +1,88 @@ +import {AutoComplete} from "@web/core/autocomplete/autocomplete"; +import {ContributorsRender} from "@contributors_github/components/contributors_render/contributors_render.esm"; +import {_t} from "@web/core/l10n/translation"; +import {onMounted} from "@odoo/owl"; +import {patch} from "@web/core/utils/patch"; +import {useService} from "@web/core/utils/hooks"; + +patch(ContributorsRender, { + components: { + ...ContributorsRender.components, + AutoComplete, + }, +}); +patch(ContributorsRender.prototype, { + setup() { + super.setup(); + this.state.rawLangs = []; + this.state.lang = ""; + this.state.langs = []; + this.state.langId = null; + this.state.translations = []; + this.state.sort.translations = "translations"; + this.selectLanguage = _t("Select Language"); + this.orm = useService("orm"); + onMounted(this.fetchLangs.bind(this)); + }, + async fetchData() { + const data = await super.fetchData(); + if (this.state.kind === "translations") { + this.state.translations = data.data; + } + return data; + }, + async fetchLangs() { + const langs = await this.orm.searchRead( + "res.lang", + [["active", "in", [true, false]]], + ["id", "name"] + ); + this.state.rawLangs = Object.fromEntries(langs.map((lang) => [lang.id, lang])); + this.state.langs = langs.map((lang) => { + return { + label: lang.name, + value: lang.id, + }; + }); + }, + getRowData(row_id) { + if (this.state.kind === "translations") { + return this.state.translations[row_id]; + } + return super.getRowData(row_id); + }, + get rowIds() { + if (this.state.kind === "translations") { + return Object.keys(this.state.translations).sort( + (a, b) => + this.state.translations[b][this.state.sort.translations] - + this.state.translations[a][this.state.sort.translations] + ); + } + return super.rowIds; + }, + get langSources() { + const langs = this.state.langs; + return [ + { + async options(query) { + return langs.filter((lang) => + lang.label.toLowerCase().includes(query.toLowerCase()) + ); + }, + }, + ]; + }, + onSelectLang(option) { + this.state.langId = option.value; + this.state.lang = this.state.rawLangs[option.value].name; + this.fetchData(); + }, + getParameters() { + const params = super.getParameters(); + if (this.state.langId) { + params.lang_id = this.state.langId; + } + return params; + }, +}); diff --git a/contributors_weblate/static/src/components/contributors_render/contributors_render.xml b/contributors_weblate/static/src/components/contributors_render/contributors_render.xml new file mode 100644 index 0000000..216df89 --- /dev/null +++ b/contributors_weblate/static/src/components/contributors_render/contributors_render.xml @@ -0,0 +1,28 @@ + + + + + +
+ +
+
+ +
+ +
+
+
+ +
diff --git a/contributors_weblate/views/contributors_organization.xml b/contributors_weblate/views/contributors_organization.xml new file mode 100644 index 0000000..7f9e088 --- /dev/null +++ b/contributors_weblate/views/contributors_organization.xml @@ -0,0 +1,43 @@ + + + + + contributors.organization + + + +
+
+ + + + + + + + + + +
+
+
diff --git a/contributors_weblate/views/contributors_repository.xml b/contributors_weblate/views/contributors_repository.xml new file mode 100644 index 0000000..b2ecde6 --- /dev/null +++ b/contributors_weblate/views/contributors_repository.xml @@ -0,0 +1,24 @@ + + + + + contributors.repository + + +
+
+
+
+
diff --git a/contributors_weblate/views/contributors_translation.xml b/contributors_weblate/views/contributors_translation.xml new file mode 100644 index 0000000..d69c579 --- /dev/null +++ b/contributors_weblate/views/contributors_translation.xml @@ -0,0 +1,90 @@ + + + + + contributors.translation + +
+
+ + + + + + + + + + + + + + + + + contributors.translation + + + + + + + + + + + + + + + + + contributors.translation + + + + + + + + + + + + + + Translations + contributors.translation + list,form + [('organization_id', '=', active_id)] + {} + + + + Translations + contributors.translation + list,form + [('repository_id', '=', active_id)] + {} + + From 4858ea6999116d8826956cac00d18b394e7a0558 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 10 Jan 2026 16:11:07 +0100 Subject: [PATCH 07/44] [IMP] contributors_weblate: make message as debug --- contributors_weblate/models/contributors_organization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors_weblate/models/contributors_organization.py b/contributors_weblate/models/contributors_organization.py index d6fb894..1248336 100644 --- a/contributors_weblate/models/contributors_organization.py +++ b/contributors_weblate/models/contributors_organization.py @@ -87,7 +87,7 @@ def get_weblate_data(self): translation.sudo().write(vals) total += 1 if total % 100 == 0: - _logger.info( + _logger.debug( f"Processed {total} of {max_total} Weblate translation changes" ) if req.json().get("next"): From c6eb4df56983be91a3e12ce6119facb08022afbc Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 10 Jan 2026 16:12:10 +0100 Subject: [PATCH 08/44] [FIX] contributors_github_psc: add sudo --- contributors_github_psc/models/contributors_organization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contributors_github_psc/models/contributors_organization.py b/contributors_github_psc/models/contributors_organization.py index b649fbb..7a229d6 100644 --- a/contributors_github_psc/models/contributors_organization.py +++ b/contributors_github_psc/models/contributors_organization.py @@ -65,7 +65,9 @@ def update_information(self): members.append( self.env["res.partner"]._get_github_user(member, client) ) - psc_record.member_ids = self.env["res.partner"].browse(members) + psc_record.sudo().member_ids = self.env["res.partner"].browse( + members + ) for _repo_filename, repo_file in repository_files: req = requests.get(repo_file.download_url, timeout=10) req.raise_for_status() From e561e93e101d082815f6bae203d0c2f09b0e4153 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 10 Jan 2026 19:45:38 +0100 Subject: [PATCH 09/44] [FIX] contributors_weblate: Fix unsettling language --- .../contributors_render/contributors_render.esm.js | 7 +++++++ .../components/contributors_render/contributors_render.xml | 1 + 2 files changed, 8 insertions(+) diff --git a/contributors_weblate/static/src/components/contributors_render/contributors_render.esm.js b/contributors_weblate/static/src/components/contributors_render/contributors_render.esm.js index ba11ec6..fdf5701 100644 --- a/contributors_weblate/static/src/components/contributors_render/contributors_render.esm.js +++ b/contributors_weblate/static/src/components/contributors_render/contributors_render.esm.js @@ -73,6 +73,13 @@ patch(ContributorsRender.prototype, { }, ]; }, + onChangeLang(event) { + if (event.inputValue === "") { + this.state.langId = null; + this.state.lang = ""; + this.fetchData(); + } + }, onSelectLang(option) { this.state.langId = option.value; this.state.lang = this.state.rawLangs[option.value].name; diff --git a/contributors_weblate/static/src/components/contributors_render/contributors_render.xml b/contributors_weblate/static/src/components/contributors_render/contributors_render.xml index 216df89..ec44317 100644 --- a/contributors_weblate/static/src/components/contributors_render/contributors_render.xml +++ b/contributors_weblate/static/src/components/contributors_render/contributors_render.xml @@ -8,6 +8,7 @@ value="state.lang" sources="langSources" onSelect.bind="onSelectLang" + onChange.bind="onChangeLang" class="" resetOnSelect="true" autoSelect="true" From f40458add47cbf3261785394e67efd0d88bc14f8 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 10 Jan 2026 19:46:30 +0100 Subject: [PATCH 10/44] [FIX] contributors_github_psc: Allow unsetting PSC --- .../contributors_render/contributors_render.esm.js | 7 +++++++ .../components/contributors_render/contributors_render.xml | 1 + 2 files changed, 8 insertions(+) diff --git a/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js b/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js index dc7de60..dc60812 100644 --- a/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js +++ b/contributors_github_psc/static/src/components/contributors_render/contributors_render.esm.js @@ -47,6 +47,13 @@ patch(ContributorsRender.prototype, { }, ]; }, + onChangePsc(event) { + if (event.inputValue === "") { + this.state.pscId = null; + this.state.psc = ""; + this.fetchData(); + } + }, onSelectPsc(option) { this.state.pscId = option.value; this.state.psc = this.state.rawPscs[option.value].name; diff --git a/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml b/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml index 17fbecb..b9abca0 100644 --- a/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml +++ b/contributors_github_psc/static/src/components/contributors_render/contributors_render.xml @@ -8,6 +8,7 @@ value="state.psc" sources="pscSources" onSelect.bind="onSelectPsc" + onChange.bind="onChangePsc" class="" resetOnSelect="true" autoSelect="true" From 22b23b56535fff9a2257cf9ce299bf4417875ce5 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 10 Jan 2026 19:54:18 +0100 Subject: [PATCH 11/44] [FIX] contributors_weblate: add other options --- contributors_weblate/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors_weblate/controllers/main.py b/contributors_weblate/controllers/main.py index b02cd4a..c59ea2f 100644 --- a/contributors_weblate/controllers/main.py +++ b/contributors_weblate/controllers/main.py @@ -32,7 +32,7 @@ def _get_default_data(self, organization, start, end, field, kind, **values): return data def _translation_actions(self): - return ["5", "8", "25", "27"] + return ["2", "5", "8", "25", "27"] def _get_translation_domain( self, From 2c34b56f1987355099a1417c3f96f664c8b0584a Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 12 Jan 2026 09:05:57 +0100 Subject: [PATCH 12/44] [FIX] contributors_github: Fill comments on creation too... --- .../models/contributors_repository.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/contributors_github/models/contributors_repository.py b/contributors_github/models/contributors_repository.py index f6b7487..0ce5f13 100644 --- a/contributors_github/models/contributors_repository.py +++ b/contributors_github/models/contributors_repository.py @@ -166,21 +166,21 @@ def update_information(self, update_interval_days=None): ) else: opr.sudo().write(pr_data) - for comment in comments: - comment_id = comment.pop("id") - ocomment = self.env["contributors.comment"].search( - [("github_id", "=", comment_id)], limit=1 + for comment in comments: + comment_id = comment.pop("id") + ocomment = self.env["contributors.comment"].search( + [("github_id", "=", comment_id)], limit=1 + ) + if not ocomment: + self.env["contributors.comment"].sudo().create( + { + "github_id": comment_id, + "pull_request_id": opr.id, + **comment, + } ) - if not ocomment: - self.env["contributors.comment"].sudo().create( - { - "github_id": comment_id, - "pull_request_id": opr.id, - **comment, - } - ) - else: - ocomment.sudo().write(comment) + else: + ocomment.sudo().write(comment) for review in reviews: review_id = review.pop("id") oreview = self.env["contributors.review"].search( From 80e5118f1ad51dabada8b530baf092ce8ef50a20 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 12 Jan 2026 16:16:24 +0100 Subject: [PATCH 13/44] [IMP] contributors_github: Add an option to handle the search for improving the handling with multiple searches --- contributors_github/models/contributors_repository.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contributors_github/models/contributors_repository.py b/contributors_github/models/contributors_repository.py index 0ce5f13..d30ccda 100644 --- a/contributors_github/models/contributors_repository.py +++ b/contributors_github/models/contributors_repository.py @@ -124,7 +124,7 @@ def parse_pr(self, pr, client): def force_update_information(self): self.update_information(update_interval_days=365) - def update_information(self, update_interval_days=None): + def update_information(self, update_interval_days=None, client_for_search=0): self.ensure_one() clients = self.organization_id._get_clients() try: @@ -140,8 +140,8 @@ def update_information(self, update_interval_days=None): start += timedelta( days=-1 ) # Add buffer day to avoid missing PRs on boundary dates - i = 0 - for pr in clients[0].search_issues( + i = client_for_search % len(clients) + for pr in clients[i].search_issues( f"is:pr repo:{self.organization_id.name}/{self.name} " f"updated:{start.isoformat()}..{end.isoformat()}" ): @@ -199,7 +199,7 @@ def update_information(self, update_interval_days=None): self.sudo().from_date = end.replace(tzinfo=None) except github3.exceptions.ForbiddenError as e: _logger.error(e) - rate = clients[0].rate_limit() + rate = clients[i].rate_limit() reset = fields.Datetime.to_string( datetime.utcfromtimestamp(rate["resources"]["core"]["reset"]) ) From e28d52ce856cc1b7ca7e67fdb099266e7b985b1d Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 13 Jan 2026 13:40:10 +0100 Subject: [PATCH 14/44] [IMP] contributors_github: Add some cache and indexes --- .../models/contributors_branch.py | 3 +- .../models/contributors_pull_request.py | 2 +- .../models/contributors_repository.py | 2 +- .../models/contributors_review.py | 4 +- contributors_github/models/res_partner.py | 108 ++++++++++-------- 5 files changed, 68 insertions(+), 51 deletions(-) diff --git a/contributors_github/models/contributors_branch.py b/contributors_github/models/contributors_branch.py index 8d85766..f2573e6 100644 --- a/contributors_github/models/contributors_branch.py +++ b/contributors_github/models/contributors_branch.py @@ -1,7 +1,7 @@ # Copyright 2026 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import fields, models, tools class ContributorsBranch(models.Model): @@ -18,6 +18,7 @@ class ContributorsBranch(models.Model): ("name_uniq", "unique(name, organization_id)", "Branch name must be unique.") ] + @tools.cache("organization.id", "name") def _get_branch(self, organization, name): branch = self.search( [("organization_id", "=", organization.id), ("name", "=", name)], diff --git a/contributors_github/models/contributors_pull_request.py b/contributors_github/models/contributors_pull_request.py index f882172..c83a03b 100644 --- a/contributors_github/models/contributors_pull_request.py +++ b/contributors_github/models/contributors_pull_request.py @@ -8,7 +8,7 @@ class ContributorsPullRequest(models.Model): _name = "contributors.pull.request" _description = "Contributors Pull Request" # TODO - github_id = fields.Char(string="GitHub ID", readonly=True) + github_id = fields.Char(string="GitHub ID", readonly=True, index=True) name = fields.Char(readonly=True) partner_id = fields.Many2one( comodel_name="res.partner", diff --git a/contributors_github/models/contributors_repository.py b/contributors_github/models/contributors_repository.py index d30ccda..33696eb 100644 --- a/contributors_github/models/contributors_repository.py +++ b/contributors_github/models/contributors_repository.py @@ -17,7 +17,7 @@ class ContributorsRepository(models.Model): _name = "contributors.repository" _description = "Contributors Repository" - name = fields.Char(required=True) + name = fields.Char(required=True, index=True) description = fields.Char(readonly=True) organization_id = fields.Many2one( comodel_name="contributors.organization", diff --git a/contributors_github/models/contributors_review.py b/contributors_github/models/contributors_review.py index 453f3d8..b6e0c21 100644 --- a/contributors_github/models/contributors_review.py +++ b/contributors_github/models/contributors_review.py @@ -8,7 +8,9 @@ class ContributorsReview(models.Model): _name = "contributors.review" _description = "Contributors Review" # TODO - github_id = fields.Char(string="GitHub ID", readonly=True, required=True) + github_id = fields.Char( + string="GitHub ID", readonly=True, required=True, index=True + ) body = fields.Html(readonly=True) state = fields.Char(readonly=True) partner_id = fields.Many2one("res.partner", readonly=True) diff --git a/contributors_github/models/res_partner.py b/contributors_github/models/res_partner.py index fd0a039..2a3cf5e 100644 --- a/contributors_github/models/res_partner.py +++ b/contributors_github/models/res_partner.py @@ -3,7 +3,7 @@ import github3 -from odoo import fields, models +from odoo import fields, models, tools class ResPartner(models.Model): @@ -33,63 +33,77 @@ class ResPartner(models.Model): ), ] - def _get_github_user(self, gh, client): - if not gh: - return False - github_login = str(gh) + @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: - if isinstance(gh, str): - try: - gh = client.user(github_login) - name = gh.name or github_login - except github3.exceptions.NotFoundError: - name = gh - else: - name = gh.name or github_login - return self.create( - { - "name": name, - "github_name": github_login, - "github_user": True, - } - ).id + return False if not partner.github_user: partner.github_user = True return partner.id - def _get_github_organization(self, gh, client): - if not gh: - return False + @tools.ormcache("github_login") + def _get_github_organization_id(self, github_login): partner = self.with_context(active_test=False).search( - [("github_name", "=ilike", str(gh))], limit=1 + [("github_name", "=ilike", github_login)], limit=1 ) if not partner: - try: - org = client.organization(str(gh)) - if org: - 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 + 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 + 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: + name = gh.name or github_login + 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 + try: + org = client.organization(str(gh)) + if org: + 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 From 85df898de1ad3f480bf00690fceca56c9ab6d8a7 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 13 Jan 2026 13:40:22 +0100 Subject: [PATCH 15/44] [IMP] contributors_github: Add some cache and indexes --- contributors_github/models/contributors_review.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contributors_github/models/contributors_review.py b/contributors_github/models/contributors_review.py index b6e0c21..bf615fa 100644 --- a/contributors_github/models/contributors_review.py +++ b/contributors_github/models/contributors_review.py @@ -8,9 +8,7 @@ class ContributorsReview(models.Model): _name = "contributors.review" _description = "Contributors Review" # TODO - github_id = fields.Char( - string="GitHub ID", readonly=True, required=True, index=True - ) + github_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) From a687826acef084265cc780ba1309d057df22171f Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 13 Jan 2026 13:41:12 +0100 Subject: [PATCH 16/44] [FIX] contributors_weblate: Add index --- .../models/contributors_translation.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contributors_weblate/models/contributors_translation.py b/contributors_weblate/models/contributors_translation.py index 8ef0b7a..2de59a9 100644 --- a/contributors_weblate/models/contributors_translation.py +++ b/contributors_weblate/models/contributors_translation.py @@ -8,7 +8,7 @@ class ContributorsTranslation(models.Model): _name = "contributors.translation" _description = "Contributors Translation" # TODO - integration_id = fields.Char() + integration_id = fields.Char(required=True, index=True) unit = fields.Char() translation = fields.Char() component = fields.Char() @@ -97,3 +97,11 @@ class ContributorsTranslation(models.Model): ] ) details = fields.Json() + + _sql_constraints = [ + ( + "integration_id_uniq", + "unique(integration_id, organization_id)", + "The integration ID must be unique.", + ), + ] From 9c920a4237a8100bb89505be4e01b25f0691caeb Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 13 Jan 2026 13:43:42 +0100 Subject: [PATCH 17/44] [FIX] contributors_github: add index --- contributors_github/models/contributors_comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors_github/models/contributors_comment.py b/contributors_github/models/contributors_comment.py index 82a6539..59e4741 100644 --- a/contributors_github/models/contributors_comment.py +++ b/contributors_github/models/contributors_comment.py @@ -8,7 +8,7 @@ class ContributorsComment(models.Model): _name = "contributors.comment" _description = "Contributors Comment" # TODO - github_id = fields.Char(string="GitHub ID", readonly=True, required=True) + github_id = fields.Char(readonly=True, required=True, index=True) body = fields.Html(readonly=True) partner_id = fields.Many2one( comodel_name="res.partner", From 26287d7639e31b1e26363788501e448a1365c33a Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 13 Jan 2026 13:47:27 +0100 Subject: [PATCH 18/44] [FIX] contributors_github: Pass logic to organization --- .../models/contributors_branch.py | 17 +-------------- .../models/contributors_organization.py | 21 ++++++++++++++++++- .../models/contributors_repository.py | 4 +--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/contributors_github/models/contributors_branch.py b/contributors_github/models/contributors_branch.py index f2573e6..29421f0 100644 --- a/contributors_github/models/contributors_branch.py +++ b/contributors_github/models/contributors_branch.py @@ -1,7 +1,7 @@ # Copyright 2026 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models, tools +from odoo import fields, models class ContributorsBranch(models.Model): @@ -17,18 +17,3 @@ class ContributorsBranch(models.Model): _sql_constraints = [ ("name_uniq", "unique(name, organization_id)", "Branch name must be unique.") ] - - @tools.cache("organization.id", "name") - def _get_branch(self, organization, name): - branch = self.search( - [("organization_id", "=", organization.id), ("name", "=", name)], - limit=1, - ) - if not branch: - branch = self.sudo().create( - { - "organization_id": organization.id, - "name": name, - } - ) - return branch.id diff --git a/contributors_github/models/contributors_organization.py b/contributors_github/models/contributors_organization.py index 56f06a1..8501612 100644 --- a/contributors_github/models/contributors_organization.py +++ b/contributors_github/models/contributors_organization.py @@ -6,7 +6,7 @@ import github3 import requests -from odoo import fields, models +from odoo import fields, models, tools _logger = logging.getLogger(__name__) @@ -80,6 +80,25 @@ def _cron_update_organizations(self): "Error updating organization %s: %s", organization.name, str(e) ) + @tools.cache("self.id", "name") + def _get_branch(self, name): + branch = self.env["contributors.branch"].search( + [("organization_id", "=", self.id), ("name", "=", name)], + limit=1, + ) + if not branch: + branch = ( + self.env["contributors.branch"] + .sudo() + .create( + { + "organization_id": self.id, + "name": name, + } + ) + ) + return branch.id + class ContributorsOrganizationKey(models.Model): _name = "contributors.organization.key" diff --git a/contributors_github/models/contributors_repository.py b/contributors_github/models/contributors_repository.py index 33696eb..00207f2 100644 --- a/contributors_github/models/contributors_repository.py +++ b/contributors_github/models/contributors_repository.py @@ -61,9 +61,7 @@ def parse_pr(self, pr, client): { "partner_id": self.env["res.partner"]._get_github_user(pr.user, client), "repository_id": self.id, - "branch_id": self.env["contributors.branch"]._get_branch( - self.organization_id, pr.base.ref - ), + "branch_id": self.organization_id._get_branch(pr.base.ref), "organization_id": self.env["res.partner"]._get_github_organization( pr.head.repo[0], client ), From ed217d5d812e26e4bec4d9fe837c3261a3dd008b Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 13 Jan 2026 13:47:45 +0100 Subject: [PATCH 19/44] [FIX] contributors_weblate: Use a different name for function --- contributors_weblate/models/contributors_organization.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contributors_weblate/models/contributors_organization.py b/contributors_weblate/models/contributors_organization.py index 1248336..7de0733 100644 --- a/contributors_weblate/models/contributors_organization.py +++ b/contributors_weblate/models/contributors_organization.py @@ -76,7 +76,7 @@ def get_weblate_data(self): ), "action": str(result["action"]), "repository_id": self._get_repository(result["component"]), - "branch_id": self._get_branch(result["component"]), + "branch_id": self._get_weblate_branch(result["component"]), "lang_id": self._get_lang(result["translation"]), } if not translation: @@ -134,7 +134,7 @@ def _get_lang(self, translation): ) @tools.ormcache("self.id", "component") - def _get_branch(self, component): + def _get_weblate_branch(self, component): if not component: return False parts = component.split("/") @@ -143,9 +143,7 @@ def _get_branch(self, component): found = re.match(r"^[\w-]+-(\d+-\d)$", parts[-3]) if not found: return False - return self.env["contributors.branch"]._get_branch( - self, found.group(1).replace("-", ".") - ) + return self._get_branch(found.group(1)) @tools.ormcache("self.id", "component") def _get_repository(self, component): From 1d9b6ccfb5c070d04c56f3f8ee7e249395638a4f Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 13 Jan 2026 13:53:03 +0100 Subject: [PATCH 20/44] [FIX] contributors_github: use ormcache... --- contributors_github/models/contributors_organization.py | 2 +- contributors_github/models/contributors_pull_request.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contributors_github/models/contributors_organization.py b/contributors_github/models/contributors_organization.py index 8501612..3bec919 100644 --- a/contributors_github/models/contributors_organization.py +++ b/contributors_github/models/contributors_organization.py @@ -80,7 +80,7 @@ def _cron_update_organizations(self): "Error updating organization %s: %s", organization.name, str(e) ) - @tools.cache("self.id", "name") + @tools.ormcache("self.id", "name") def _get_branch(self, name): branch = self.env["contributors.branch"].search( [("organization_id", "=", self.id), ("name", "=", name)], diff --git a/contributors_github/models/contributors_pull_request.py b/contributors_github/models/contributors_pull_request.py index c83a03b..5b440ee 100644 --- a/contributors_github/models/contributors_pull_request.py +++ b/contributors_github/models/contributors_pull_request.py @@ -60,7 +60,7 @@ class ContributorsPullRequestLabel(models.Model): _sql_constraints = [("name_uniq", "unique(name)", "Label name must be unique.")] - @tools.ormcache() + @tools.ormcache("name") def _get_label(self, name): label = self.search([("name", "=", name)], limit=1) if not label: From aba23816802caafd78a963f1e60631f27ea62b0d Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 14 Jan 2026 07:20:54 +0100 Subject: [PATCH 21/44] [FIX] contributors_github: Use cache properly and remove it if necessary --- contributors_github/models/res_partner.py | 28 +++++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/contributors_github/models/res_partner.py b/contributors_github/models/res_partner.py index 2a3cf5e..efab01d 100644 --- a/contributors_github/models/res_partner.py +++ b/contributors_github/models/res_partner.py @@ -58,7 +58,10 @@ def _get_github_organization_id(self, github_login): def _get_github_user(self, gh, client): if not gh: return False - github_login = str(gh) + 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 @@ -69,7 +72,11 @@ def _get_github_user(self, gh, client): except github3.exceptions.NotFoundError: name = gh else: - name = gh.name or github_login + if hasattr(gh, "name"): + name = gh.name or github_login + else: + name = github_login + self.env.registry.clear_cache() return self.create( { "name": name, @@ -84,16 +91,17 @@ def _get_github_organization(self, gh, client): partner = self._get_github_organization_id(str(gh)) if partner: return partner + self.env.registry.clear_cache() try: org = client.organization(str(gh)) - if org: - return self.create( - { - "name": org.name or str(gh), - "github_name": str(gh), - "github_organization": True, - } - ).id + + 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) From 0ce2a436718c6f91dc019c79c699b0477e842ba6 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 14 Jan 2026 10:23:35 +0100 Subject: [PATCH 22/44] [IMP] contributors_github: Add number of developers --- contributors_github/controllers/main.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/contributors_github/controllers/main.py b/contributors_github/controllers/main.py index 4e523c9..7927085 100644 --- a/contributors_github/controllers/main.py +++ b/contributors_github/controllers/main.py @@ -181,6 +181,12 @@ def _get_columns(self, kind): "kind": "float", "decimals": 0, }, + { + "field": "developers", + "title": _("Developers"), + "kind": "float", + "decimals": 0, + }, ] elif kind == "repositories": return [ @@ -215,6 +221,12 @@ def _get_columns(self, kind): "kind": "float", "decimals": 0, }, + { + "field": "developers", + "title": _("Developers"), + "kind": "float", + "decimals": 0, + }, ] return [] @@ -227,6 +239,7 @@ def _get_default_data(self, organization, start, end, field, kind, **values): "merged_pull_requests": 0, "comments": 0, "reviews": 0, + "developers": 0, } def _generate_data(self, organization, start, end, field, kind, **values): @@ -253,11 +266,15 @@ def _generate_data(self, organization, start, end, field, kind, **values): .read_group( self._get_created_domain(organization, start, end, **values) + [(field, "!=", False)], - [field], + [field, "partner_id:count_distinct"] + if field != "partner_id" + else [field], [field], ) ): data[pr[field][0]]["created_pull_requests"] = pr[f"{field}_count"] + if field != "partner_id": + data[pr[field][0]]["developers"] = pr["partner_id"] for comment in ( request.env["contributors.comment"] .sudo() From 7115554bd6a96cbcfa80872ce1186c7192975e9a Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 14 Jan 2026 11:59:31 +0100 Subject: [PATCH 23/44] [FIX] contributors_weblate: add actions for better filtering... --- contributors_weblate/controllers/main.py | 5 +---- contributors_weblate/models/contributors_organization.py | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/contributors_weblate/controllers/main.py b/contributors_weblate/controllers/main.py index c59ea2f..e1cf292 100644 --- a/contributors_weblate/controllers/main.py +++ b/contributors_weblate/controllers/main.py @@ -31,9 +31,6 @@ def _get_default_data(self, organization, start, end, field, kind, **values): data["translations"] = 0 return data - def _translation_actions(self): - return ["2", "5", "8", "25", "27"] - def _get_translation_domain( self, organization, @@ -45,7 +42,7 @@ def _get_translation_domain( **values, ): if actions is None: - actions = self._translation_actions() + actions = request.env["contributors.organization"]._translation_actions() domain = [ ("organization_id", "=", organization.id), ("date", ">=", start), diff --git a/contributors_weblate/models/contributors_organization.py b/contributors_weblate/models/contributors_organization.py index 7de0733..e420c70 100644 --- a/contributors_weblate/models/contributors_organization.py +++ b/contributors_weblate/models/contributors_organization.py @@ -6,7 +6,7 @@ import requests -from odoo import _, fields, models, tools +from odoo import _, api, fields, models, tools from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) @@ -22,6 +22,10 @@ class ContributorsOrganization(models.Model): help="Number of days to load each time", default=7 ) + @api.model + def _translation_actions(self): + return ["2", "5", "8", "25", "27"] + def get_weblate_data(self): self.ensure_one() if not self.weblate_last_update: @@ -43,6 +47,7 @@ def get_weblate_data(self): ).isoformat(), "timestamp_before": before_date.isoformat(), "format": "json", + "action": self._translation_actions(), }, timeout=10, headers=auth, From 6803b6f8bb977b0f7741392d251085147c1dbfed Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 14 Jan 2026 14:05:59 +0100 Subject: [PATCH 24/44] [IMP] contributors_weblate: add status verification --- contributors_weblate/models/contributors_organization.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contributors_weblate/models/contributors_organization.py b/contributors_weblate/models/contributors_organization.py index e420c70..f3c07a0 100644 --- a/contributors_weblate/models/contributors_organization.py +++ b/contributors_weblate/models/contributors_organization.py @@ -52,10 +52,18 @@ def get_weblate_data(self): timeout=10, headers=auth, ) + req.raise_for_status() # self.weblate_last_update = before_date total = 0 client = self._get_clients()[0] while True: + if req.json().get("errors"): + raise ValidationError( + _( + "Error while fetching Weblate data for organization " + f"{self.name}: {req.json().get('errors')}" + ) + ) results = req.json().get("results", []) max_total = req.json().get("count", 0) for result in results: @@ -97,6 +105,7 @@ def get_weblate_data(self): ) if req.json().get("next"): req = requests.get(req.json().get("next"), timeout=10, headers=auth) + req.raise_for_status() else: break self.weblate_last_update = before_date From 7d783110ebc3623b7f3651267d6681a5169a5a67 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 14 Jan 2026 19:23:47 +0100 Subject: [PATCH 25/44] [IMP] contributors_github: Add tooltips --- contributors_github/controllers/main.py | 7 +++++ .../contributors_render.esm.js | 24 +++++++++----- .../contributors_render.xml | 31 +++++++++++++++++-- .../popover_tooltip/popover_tooltip.esm.js | 8 +++++ .../popover_tooltip/popover_tooltip.xml | 7 +++++ contributors_github/templates/templates.xml | 23 ++++++++++++++ 6 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 contributors_github/static/src/components/popover_tooltip/popover_tooltip.esm.js create mode 100644 contributors_github/static/src/components/popover_tooltip/popover_tooltip.xml diff --git a/contributors_github/controllers/main.py b/contributors_github/controllers/main.py index 7927085..b1c0e4f 100644 --- a/contributors_github/controllers/main.py +++ b/contributors_github/controllers/main.py @@ -6,6 +6,7 @@ from math import sqrt from dateutil.relativedelta import relativedelta +from markupsafe import Markup from odoo import _, http from odoo.http import request @@ -128,6 +129,11 @@ def _get_columns(self, kind): "title": _("Contributor Index"), "kind": "float", "decimals": 2, + "tooltip": Markup( + request.env["ir.qweb"]._render( + "contributors_github.contributor_index_tooltip", {} + ) + ), }, { "field": "created_pull_requests", @@ -196,6 +202,7 @@ def _get_columns(self, kind): "title": _("Repository Index"), "kind": "float", "decimals": 2, + "tooltip": Markup("
HOLA
"), }, { "field": "created_pull_requests", diff --git a/contributors_github/static/src/components/contributors_render/contributors_render.esm.js b/contributors_github/static/src/components/contributors_render/contributors_render.esm.js index a13e456..bf2512e 100644 --- a/contributors_github/static/src/components/contributors_render/contributors_render.esm.js +++ b/contributors_github/static/src/components/contributors_render/contributors_render.esm.js @@ -1,18 +1,15 @@ -import {Component, onMounted, useState} from "@odoo/owl"; - +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"; -/** - * This Component is a signature request form. It uses - * @see NameAndSignature for the input fields, adds a submit - * button, and handles the RPC to save the result. - */ export class ContributorsRender extends Component { - static template = "cotributors_github.ContributorsRender"; + static template = "contributors_github.ContributorsRender"; setup() { const year = new Date().getFullYear(); const month = new Date().getMonth() + 1; @@ -32,11 +29,22 @@ export class ContributorsRender extends Component { 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("contributors_github.DateSelectionTooltip")), + }); + } async fetchData() { const data = await rpc("/contributors/fetch", this.getParameters()); if (this.state.kind === "contributors") { diff --git a/contributors_github/static/src/components/contributors_render/contributors_render.xml b/contributors_github/static/src/components/contributors_render/contributors_render.xml index 989a60a..835f6bc 100644 --- a/contributors_github/static/src/components/contributors_render/contributors_render.xml +++ b/contributors_github/static/src/components/contributors_render/contributors_render.xml @@ -1,7 +1,17 @@ - - - + +
+ 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

@@ -40,6 +50,13 @@ class="ms-4 px-3 btn btn-link btn-primary py-0 px-1 o-dropdown-caret h-100" > + + + +
+ +
+
+ diff --git a/contributors_github/templates/templates.xml b/contributors_github/templates/templates.xml index 1bd925d..cc0f84c 100644 --- a/contributors_github/templates/templates.xml +++ b/contributors_github/templates/templates.xml @@ -8,6 +8,29 @@ />
+ + + From 811561db4230ffbf6e22ca79378cba30abfd5387 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Thu, 22 Jan 2026 19:05:58 +0100 Subject: [PATCH 36/44] [IMP] contributors_weblate: Add portal view --- contributors_weblate/__manifest__.py | 1 + contributors_weblate/controllers/main.py | 61 ------------------- contributors_weblate/models/__init__.py | 1 + .../models/contributors_organization.py | 52 ++++++++++++++++ contributors_weblate/models/res_partner.py | 18 ++++++ contributors_weblate/templates/templates.xml | 14 +++++ 6 files changed, 86 insertions(+), 61 deletions(-) create mode 100644 contributors_weblate/models/res_partner.py create mode 100644 contributors_weblate/templates/templates.xml diff --git a/contributors_weblate/__manifest__.py b/contributors_weblate/__manifest__.py index 70faaa0..a604454 100644 --- a/contributors_weblate/__manifest__.py +++ b/contributors_weblate/__manifest__.py @@ -17,6 +17,7 @@ "views/contributors_translation.xml", "views/contributors_repository.xml", "views/contributors_organization.xml", + "templates/templates.xml", ], "demo": [], "assets": { diff --git a/contributors_weblate/controllers/main.py b/contributors_weblate/controllers/main.py index e1cf292..19e4fa2 100644 --- a/contributors_weblate/controllers/main.py +++ b/contributors_weblate/controllers/main.py @@ -24,38 +24,6 @@ def _get_columns(self, kind): ) return columns - def _get_default_data(self, organization, start, end, field, kind, **values): - data = super()._get_default_data( - organization, start, end, field, kind, **values - ) - data["translations"] = 0 - return data - - def _get_translation_domain( - self, - organization, - start, - end, - psc_id=None, - lang_id=None, - actions=None, - **values, - ): - if actions is None: - actions = request.env["contributors.organization"]._translation_actions() - domain = [ - ("organization_id", "=", organization.id), - ("date", ">=", start), - ("date", "<", end), - ("action", "in", actions), - ] - if lang_id: - domain.append(("lang_id", "=", lang_id)) - if psc_id and "psc_id" in request.env["contributors.repository"]._fields: - # To avoid an extra module we add the psc_id filter here - domain.append(("repository_id.psc_id", "=", int(psc_id))) - return domain - def _improve_data(self, data, kind, **kwargs): data = super()._improve_data(data, kind, **kwargs) if kind == "translations": @@ -64,32 +32,3 @@ def _improve_data(self, data, kind, **kwargs): values["name"] = self._get_partner_name(partner, **kwargs) values["github_name"] = partner.github_name return data - - def _generate_data( - self, organization, start, end, field, kind, lang_id=None, **values - ): - data = super()._generate_data(organization, start, end, field, kind, **values) - if kind == "translations" and not field: - field = "partner_id" - if ( - kind in ["contributors", "repositories", "translations"] - and organization.weblate_url - ): - for merged in ( - request.env["contributors.translation"] - .sudo() - .read_group( - self._get_translation_domain( - organization, - start, - end, - lang_id=lang_id if kind == "translations" else None, - **values, - ) - + [(field, "!=", False)], - [field], - [field], - ) - ): - data[merged[field][0]]["translations"] = merged[f"{field}_count"] - return data diff --git a/contributors_weblate/models/__init__.py b/contributors_weblate/models/__init__.py index 256ed35..680796e 100644 --- a/contributors_weblate/models/__init__.py +++ b/contributors_weblate/models/__init__.py @@ -1,3 +1,4 @@ from . import contributors_organization from . import contributors_translation from . import contributors_repository +from . import res_partner diff --git a/contributors_weblate/models/contributors_organization.py b/contributors_weblate/models/contributors_organization.py index f3c07a0..2973a41 100644 --- a/contributors_weblate/models/contributors_organization.py +++ b/contributors_weblate/models/contributors_organization.py @@ -188,3 +188,55 @@ def _cron_weblate_data(self): "Error while updating Weblate data for organization " f"{organization.name}: {e}" ) + + def _get_default_data(self, start, end, field, kind, **values): + data = super()._get_default_data(start, end, field, kind, **values) + data["translations"] = 0 + return data + + def _get_translation_domain( + self, + start, + end, + psc_id=None, + lang_id=None, + actions=None, + **values, + ): + if actions is None: + actions = self._translation_actions() + domain = [ + ("organization_id", "=", self.ids), + ("date", ">=", start), + ("date", "<", end), + ("action", "in", actions), + ] + if lang_id: + domain.append(("lang_id", "=", lang_id)) + if psc_id and "psc_id" in self.env["contributors.repository"]._fields: + # To avoid an extra module we add the psc_id filter here + domain.append(("repository_id.psc_id", "=", int(psc_id))) + return domain + + def _generate_data(self, start, end, field, kind, lang_id=None, **values): + data = super()._generate_data(start, end, field, kind, **values) + if kind == "translations" and not field: + field = "partner_id" + if kind in ["contributors", "repositories", "translations"]: + for merged in ( + self.env["contributors.translation"] + .sudo() + .read_group( + self._get_translation_domain( + start, + end, + lang_id=lang_id if kind == "translations" else None, + **values, + ) + + [(field, "!=", False)], + [field], + [field], + ) + ): + data[merged[field][0]]["translations"] = merged[f"{field}_count"] + return data diff --git a/contributors_weblate/models/res_partner.py b/contributors_weblate/models/res_partner.py new file mode 100644 index 0000000..54ff814 --- /dev/null +++ b/contributors_weblate/models/res_partner.py @@ -0,0 +1,18 @@ +# 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" + + weblate_translations = fields.Integer( + compute="_compute_github_contributions", string="Translations", prefetch=False + ) + + @api.model + def _get_contributors_field_map(self): + result = super()._get_contributors_field_map() + result["weblate_translations"] = "translations" + return result diff --git a/contributors_weblate/templates/templates.xml b/contributors_weblate/templates/templates.xml new file mode 100644 index 0000000..02174c3 --- /dev/null +++ b/contributors_weblate/templates/templates.xml @@ -0,0 +1,14 @@ + + + + From 2306e191e69c923087bbd53f3b649ceb0e8667bd Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Thu, 22 Jan 2026 19:06:14 +0100 Subject: [PATCH 37/44] [FIX] contributors_github_psc: adapt to new logic --- contributors_github_psc/controllers/main.py | 32 ------------------- .../models/contributors_organization.py | 24 ++++++++++++++ 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/contributors_github_psc/controllers/main.py b/contributors_github_psc/controllers/main.py index 01171a8..56a7f2f 100644 --- a/contributors_github_psc/controllers/main.py +++ b/contributors_github_psc/controllers/main.py @@ -5,38 +5,6 @@ class ContributorsPSCController(ContributorsController): - def _get_merged_domain(self, organization, start, end, psc_id=None, **values): - result = super()._get_merged_domain( - organization, start, end, psc_id=psc_id, **values - ) - if psc_id: - result.append(("repository_id.psc_id", "=", int(psc_id))) - return result - - def _get_created_domain(self, organization, start, end, psc_id=None, **values): - result = super()._get_created_domain( - organization, start, end, psc_id=psc_id, **values - ) - if psc_id: - result.append(("repository_id.psc_id", "=", int(psc_id))) - return result - - def _get_comments_domain(self, organization, start, end, psc_id=None, **values): - result = super()._get_comments_domain( - organization, start, end, psc_id=psc_id, **values - ) - if psc_id: - result.append(("repository_id.psc_id", "=", int(psc_id))) - return result - - def _get_reviews_domain(self, organization, start, end, psc_id=None, **values): - result = super()._get_reviews_domain( - organization, start, end, psc_id=psc_id, **values - ) - if psc_id: - result.append(("repository_id.psc_id", "=", int(psc_id))) - return result - def _get_partner_name(self, partner, psc_id=None, **kwargs): result = super()._get_partner_name(partner, **kwargs) if psc_id: diff --git a/contributors_github_psc/models/contributors_organization.py b/contributors_github_psc/models/contributors_organization.py index 7a229d6..160045a 100644 --- a/contributors_github_psc/models/contributors_organization.py +++ b/contributors_github_psc/models/contributors_organization.py @@ -101,3 +101,27 @@ def update_information(self): } ) return res + + def _get_merged_domain(self, start, end, psc_id=None, **values): + result = super()._get_merged_domain(start, end, psc_id=psc_id, **values) + if psc_id: + result.append(("repository_id.psc_id", "=", int(psc_id))) + return result + + def _get_created_domain(self, start, end, psc_id=None, **values): + result = super()._get_created_domain(start, end, psc_id=psc_id, **values) + if psc_id: + result.append(("repository_id.psc_id", "=", int(psc_id))) + return result + + def _get_comments_domain(self, start, end, psc_id=None, **values): + result = super()._get_comments_domain(start, end, psc_id=psc_id, **values) + if psc_id: + result.append(("repository_id.psc_id", "=", int(psc_id))) + return result + + def _get_reviews_domain(self, start, end, psc_id=None, **values): + result = super()._get_reviews_domain(start, end, psc_id=psc_id, **values) + if psc_id: + result.append(("repository_id.psc_id", "=", int(psc_id))) + return result From be590b186b85b96556da08df75cdb0a7334e69ec Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Thu, 22 Jan 2026 20:15:02 +0100 Subject: [PATCH 38/44] [FIX] contributors_github: Improve css --- contributors_github/templates/templates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contributors_github/templates/templates.xml b/contributors_github/templates/templates.xml index cc77ccb..abf3d15 100644 --- a/contributors_github/templates/templates.xml +++ b/contributors_github/templates/templates.xml @@ -111,19 +111,19 @@
-
+
Merged Pull Requests
-
+
Created Pull Requests
-
+
Comments
-
+
Reviews
From 0ba92935d7ffb199f0374e644bb9398970d5b6a8 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Thu, 22 Jan 2026 20:15:17 +0100 Subject: [PATCH 39/44] [IMP] weblate: Improve css --- contributors_weblate/templates/templates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors_weblate/templates/templates.xml b/contributors_weblate/templates/templates.xml index 02174c3..538f1cf 100644 --- a/contributors_weblate/templates/templates.xml +++ b/contributors_weblate/templates/templates.xml @@ -3,7 +3,7 @@