diff --git a/openapi_doc_viewer/README.rst b/openapi_doc_viewer/README.rst new file mode 100644 index 00000000..b6790cf4 --- /dev/null +++ b/openapi_doc_viewer/README.rst @@ -0,0 +1 @@ +waiting the bot :) diff --git a/openapi_doc_viewer/__init__.py b/openapi_doc_viewer/__init__.py new file mode 100644 index 00000000..e046e49f --- /dev/null +++ b/openapi_doc_viewer/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/openapi_doc_viewer/__manifest__.py b/openapi_doc_viewer/__manifest__.py new file mode 100644 index 00000000..8bc63e8f --- /dev/null +++ b/openapi_doc_viewer/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2026 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "OpenAPI docs viewer", + "summary": "Browse all the OpenAPI docs you can see in one page.", + "version": "18.0.1.0.0", + "license": "LGPL-3", + "development_status": "Alpha", + "author": "Camptocamp,Odoo Community Association (OCA)", + "maintainers": ["simahawk"], + "website": "https://github.com/OCA/web-api", + "depends": ["openapi_doc_base"], + "data": ["views/menus.xml"], +} diff --git a/openapi_doc_viewer/controllers/__init__.py b/openapi_doc_viewer/controllers/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/openapi_doc_viewer/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/openapi_doc_viewer/controllers/main.py b/openapi_doc_viewer/controllers/main.py new file mode 100644 index 00000000..fe4ac533 --- /dev/null +++ b/openapi_doc_viewer/controllers/main.py @@ -0,0 +1,36 @@ +# Copyright 2026 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import http +from odoo.http import request + +from odoo.addons.openapi_doc_base.http import OpenApiDocsControllerMixin + +DOCS_ROUTE = "/openapi/docs" + + +class OpenApiDocsController(http.Controller, OpenApiDocsControllerMixin): + """One page for all the docs sources the user can see.""" + + def _openapi_all_sources(self): + """All the registered sources: the mixin keeps only the visible ones.""" + return request.env["ir.http"].sudo()._openapi_doc_sources_for() + + @http.route(DOCS_ROUTE, type="http", auth="user", methods=["GET"]) + def openapi_docs(self, category=None, app=None): + """Swagger UI w/ category and app selectors.""" + return self._openapi_index_response( + self._openapi_all_sources(), + DOCS_ROUTE, + lambda source: f"{DOCS_ROUTE}/{source.key}.json", + category=category, + app=app, + ) + + @http.route( + f"{DOCS_ROUTE}/.json", type="http", auth="user", methods=["GET"] + ) + def openapi_doc(self, key): + """OpenAPI document of one source.""" + return self._openapi_document_response(self._openapi_all_sources(), key) diff --git a/openapi_doc_viewer/pyproject.toml b/openapi_doc_viewer/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/openapi_doc_viewer/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/openapi_doc_viewer/readme/CONTRIBUTORS.md b/openapi_doc_viewer/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..2b66303a --- /dev/null +++ b/openapi_doc_viewer/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Simone Orsi \<\> diff --git a/openapi_doc_viewer/readme/DESCRIPTION.md b/openapi_doc_viewer/readme/DESCRIPTION.md new file mode 100644 index 00000000..c32c0bd2 --- /dev/null +++ b/openapi_doc_viewer/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +Viewer for OpenAPI docs: one page to browse all the docs the current user can see: +`/openapi/docs` (also in *Settings > Technical > API docs*). + +The page lists the sources registered via `openapi_doc_base` +(documented controllers, configurable endpoints, apps...) +with links to filter them by category and app. + +Each source is shown only to the users in its groups +(and in the groups of its category): see `openapi_doc_base`. diff --git a/openapi_doc_viewer/readme/USAGE.md b/openapi_doc_viewer/readme/USAGE.md new file mode 100644 index 00000000..c5fe7429 --- /dev/null +++ b/openapi_doc_viewer/readme/USAGE.md @@ -0,0 +1,10 @@ +Give the *API docs: viewer* group to the users that need to read the docs +(admins have it by default). Sources or categories protected by other groups +are shown only to their members. + +Open `/openapi/docs` and use the links at the top to filter by category and app. +Each document is also available as JSON at `/openapi/docs/.json`. + +You don't need this module to expose docs at your own URL +(eg: public docs for an app): define your own controller +w/ `OpenApiDocsControllerMixin` from `openapi_doc_base`. diff --git a/openapi_doc_viewer/static/description/icon.png b/openapi_doc_viewer/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/openapi_doc_viewer/static/description/icon.png differ diff --git a/openapi_doc_viewer/tests/__init__.py b/openapi_doc_viewer/tests/__init__.py new file mode 100644 index 00000000..451f37f5 --- /dev/null +++ b/openapi_doc_viewer/tests/__init__.py @@ -0,0 +1 @@ +from . import test_http diff --git a/openapi_doc_viewer/tests/test_http.py b/openapi_doc_viewer/tests/test_http.py new file mode 100644 index 00000000..f59eccdb --- /dev/null +++ b/openapi_doc_viewer/tests/test_http.py @@ -0,0 +1,88 @@ +# Copyright 2026 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from unittest import mock + +from odoo import http +from odoo.tests.common import HttpCase, new_test_user, tagged + +from odoo.addons.openapi_doc_base.sources import DocSource + + +def _doc(title): + return lambda env: {"openapi": "3.0.3", "info": {"title": title}, "paths": {}} + + +EXTRA_SOURCES = [ + DocSource("test.a1", "A1", _doc("A1"), category="cat_a", app="app_1"), + DocSource( + "test.s1", "S1", _doc("S1"), category="cat_a", groups=("base.group_system",) + ), +] + + +class OpenApiDocTestController(http.Controller): + @http.route( + "/openapi_doc_test/documented", + type="http", + auth="public", + methods=["GET"], + api_doc={"group": "test_openapi_doc", "summary": "Documented controller"}, + ) + def documented(self): + return "" + + +@tagged("-at_install", "post_install") +class TestHttp(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env.registry.clear_cache("routing") + new_test_user( + cls.env, + login="doc_viewer", + password="doc_viewer", + groups="base.group_user,openapi_doc_base.group_openapi_doc_viewer", + ) + ir_http_cls = type(cls.env["ir.http"]) + orig = ir_http_cls._openapi_doc_sources + + def _sources(self): + return orig(self) + EXTRA_SOURCES + + patcher = mock.patch.object(ir_http_cls, "_openapi_doc_sources", _sources) + patcher.start() + cls.addClassCleanup(patcher.stop) + + def test_page(self): + """The page lists the sources the user may see, filterable by category.""" + self.authenticate("doc_viewer", "doc_viewer") + response = self.url_open("/openapi/docs") + self.assertEqual(response.status_code, 200) + # Documented controllers are collected automatically + self.assertIn("/openapi/docs/routes.test_openapi_doc.json", response.text) + self.assertIn("/openapi/docs/test.a1.json", response.text) + self.assertNotIn("test.s1.json", response.text) + self.assertIn("category=cat_a", response.text) + response = self.url_open("/openapi/docs?category=cat_a") + self.assertNotIn("routes.test_openapi_doc.json", response.text) + + def test_document(self): + """A source's document is served only to users allowed to see it.""" + self.authenticate("doc_viewer", "doc_viewer") + response = self.url_open("/openapi/docs/routes.test_openapi_doc.json") + self.assertIn("/openapi_doc_test/documented", response.json()["paths"]) + response = self.url_open("/openapi/docs/test.s1.json") + self.assertEqual(response.status_code, 404) + self.authenticate("admin", "admin") + response = self.url_open("/openapi/docs/test.s1.json") + self.assertEqual(response.json()["info"]["title"], "S1") + + def test_menu(self): + """The docs menu is restricted to the viewer group.""" + menu = self.env.ref("openapi_doc_viewer.menu_openapi_docs") + self.assertEqual( + menu.groups_id, self.env.ref("openapi_doc_base.group_openapi_doc_viewer") + ) diff --git a/openapi_doc_viewer/views/menus.xml b/openapi_doc_viewer/views/menus.xml new file mode 100644 index 00000000..2a710de4 --- /dev/null +++ b/openapi_doc_viewer/views/menus.xml @@ -0,0 +1,18 @@ + + + + + API docs + /openapi/docs + new + + + diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..2202a4a5 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-openapi_doc_base @ git+https://github.com/OCA/web-api.git@refs/pull/181/head#subdirectory=openapi_doc_base