diff --git a/cross_connect_client/README.rst b/cross_connect_client/README.rst new file mode 100644 index 0000000000..9fe0d561ee --- /dev/null +++ b/cross_connect_client/README.rst @@ -0,0 +1,114 @@ +==================== +Cross Connect Client +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f10ceaed1b91df49c3a9b4e8acdef3d412d7ce50105f6f1ca752630fc1559d8e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fserver--auth-lightgray.png?logo=github + :target: https://github.com/OCA/server-auth/tree/16.0/cross_connect_client + :alt: OCA/server-auth +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-auth-16-0/server-auth-16-0-cross_connect_client + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows this odoo instance users to connect directly on +another odoo instance where the module ``cross_connect_server`` is +installed. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +First of all after installing the module, you need to configure the +server connection. + +In order to do that, you need to go to the menu +``Settings > Technical > Cross Connect > Cross Connect Servers`` and +create a new server to connect to. + +Fill the fields with the server's information : + +- Url: The api root path (e.g. ``https://my-remote-odoo.com/api``) +- Api Key: The api-key from the ``cross_connect_server`` configuration + +Then click on the ``Sync Cross Connection`` button to check if the +connection is working and to sync the remote server's groups. + +After that, you will have to affect the remote groups to the local users +in order for them to be able to connect to the remote server. + +Once an user has a remote group, a new top level menu will appear in the +menu bar with the Cross Connect Server's name. Clicking on it will +redirect the user to the remote server logged in as the user. + +You can change each menu icon (for use with ``web_responsive`` for +instance) by setting the ``Web Icon Data`` in the server configuration. + +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 +------- + +* Akretion + +Contributors +------------ + +- Florian Mounier florian.mounier@akretion.com + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +This module is part of the `OCA/server-auth `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/cross_connect_client/__init__.py b/cross_connect_client/__init__.py new file mode 100644 index 0000000000..91c5580fed --- /dev/null +++ b/cross_connect_client/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/cross_connect_client/__manifest__.py b/cross_connect_client/__manifest__.py new file mode 100644 index 0000000000..76dc53a043 --- /dev/null +++ b/cross_connect_client/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Cross Connect Client", + "version": "16.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "summary": "Cross Connect Client allows to connect to a " + "Cross Connect Server enabled odoo instance.", + "category": "Tools", + "depends": ["server_environment"], + "website": "https://github.com/OCA/server-auth", + "data": [ + "security/ir_model_access.xml", + "views/cross_connect_server_views.xml", + ], + "maintainers": ["paradoxxxzero"], + "demo": [], + "installable": True, + "license": "AGPL-3", +} diff --git a/cross_connect_client/controllers/__init__.py b/cross_connect_client/controllers/__init__.py new file mode 100644 index 0000000000..0888dc1f38 --- /dev/null +++ b/cross_connect_client/controllers/__init__.py @@ -0,0 +1 @@ +from . import cross_connect diff --git a/cross_connect_client/controllers/cross_connect.py b/cross_connect_client/controllers/cross_connect.py new file mode 100644 index 0000000000..51975106d6 --- /dev/null +++ b/cross_connect_client/controllers/cross_connect.py @@ -0,0 +1,26 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import _ +from odoo.exceptions import UserError +from odoo.http import Controller, request, route + + +class CrossConnectController(Controller): + @route( + ["/cross_connect_server/"], + methods=["GET"], + type="http", + auth="public", + ) + def cross_connect( + self, + server_id, + **params, + ): + server = request.env["cross.connect.server"].sudo().browse(server_id) + if not server: + raise UserError(_("Server not found")) + + url = server._get_cross_connect_url() + return request.redirect(url, local=False) diff --git a/cross_connect_client/models/__init__.py b/cross_connect_client/models/__init__.py new file mode 100644 index 0000000000..d79d426eb1 --- /dev/null +++ b/cross_connect_client/models/__init__.py @@ -0,0 +1,2 @@ +from . import cross_connect_server +from . import res_groups diff --git a/cross_connect_client/models/cross_connect_server.py b/cross_connect_client/models/cross_connect_server.py new file mode 100644 index 0000000000..68d9ed0b19 --- /dev/null +++ b/cross_connect_client/models/cross_connect_server.py @@ -0,0 +1,176 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from urllib.parse import urlparse + +import requests + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class CrossConnectServer(models.Model): + _name = "cross.connect.server" + _description = "Cross Connect Server" + _inherit = "server.env.mixin" + + name = fields.Char( + required=True, compute="_compute_name", readonly=False, store=True + ) + server_url = fields.Char(required=True) + api_key = fields.Char( + required=True, + ) + group_ids = fields.One2many( + "res.groups", + inverse_name="cross_connect_server_id", + string="Cross Connect Server Groups", + readonly=True, + ) + menu_id = fields.Many2one( + "ir.ui.menu", + string="Menu", + help="Menu to display the Cross Connect Server in the menu", + compute="_compute_menu_id", + store=True, + ) + web_icon_data = fields.Binary( + compute="_compute_web_icon_data", inverse="_inverse_web_icon_data" + ) + + @api.depends("server_url") + def _compute_name(self): + for record in self: + if not record.name or record.name == "/": + try: + record.name = urlparse(record.server_url).hostname + except Exception: + record.name = "/" + + @api.depends("name", "group_ids") + def _compute_menu_id(self): + for record in self: + if not record.group_ids: + if record.menu_id: + if record.menu_id.action: + record.menu_id.action.unlink() + record.menu_id.unlink() + record.menu_id = False + continue + + menu_groups = self.env.ref("base.group_system") | record.group_ids + + if not record.menu_id: + action = self.env["ir.actions.act_url"].create( + { + "name": record.name, + "url": f"/cross_connect_server/{record.id}", + "target": "self", + } + ) + + record.menu_id = self.env["ir.ui.menu"].create( + { + "name": record.name, + "action": f"ir.actions.act_url,{action.id}", # noqa + "web_icon": "cross_connect_client,static/description/web_icon_data.png", + "groups_id": [(6, 0, menu_groups.ids)], + "sequence": 100, + } + ) + else: + record.menu_id.name = record.name + record.menu_id.groups_id = [(6, 0, menu_groups.ids)] + + @api.depends("menu_id") + def _compute_web_icon_data(self): + for record in self: + record.web_icon_data = record.menu_id.web_icon_data + + def _inverse_web_icon_data(self): + for record in self: + record.menu_id.web_icon_data = record.web_icon_data + + def _absolute_url_for(self, path): + return f"{self.server_url.rstrip('/')}/cross_connect/{path.lstrip('/')}" + + def _request(self, method, url, headers=None, data=None): + headers = headers or {} + headers["api-key"] = self.api_key + response = requests.request( + method, + self._absolute_url_for(url), + headers=headers, + json=data, + timeout=10, + ) + response.raise_for_status() + return response.json() + + def _get_cross_connect_url(self): + self.ensure_one() + groups = self.env.user.groups_id & self.group_ids + if not groups: + raise UserError(_("You are not allowed to access this server")) + + response = self._request( + "POST", + "/access", + data={ + "id": self.env.user.id, + "name": self.env.user.name, + "login": self.env.user.login, + "lang": self.env.user.lang, + "groups": [group.cross_connect_server_group_id for group in groups], + }, + ) + client_id = response.get("client_id") + token = response.get("token") + if not token: + raise UserError(_("Missing token")) + + return self._absolute_url_for(f"login/{client_id}/{token}") + + def _sync_groups(self): + self.ensure_one() + response = self._request("GET", "/sync") + remote_groups = response.get("groups", []) + # Removing groups that are not on the remote server + remote_groups_ids = {remote_group["id"] for remote_group in remote_groups} + self.group_ids.filtered( + lambda group: group.cross_connect_server_group_id not in remote_groups_ids + ).unlink() + + # Create or Update existing groups + for remote_group in remote_groups: + existing_group = self.group_ids.filtered( + lambda group: group.cross_connect_server_group_id == remote_group["id"] + ) + if existing_group: + existing_group.sudo().write( + { + "name": f"{self.name}: {remote_group['name']}", + "comment": remote_group["comment"], + } + ) + else: + self.env["res.groups"].sudo().create( + { + "cross_connect_server_id": self.id, + "cross_connect_server_group_id": remote_group["id"], + "name": f"{self.name}: {remote_group['name']}", + "comment": remote_group["comment"], + } + ) + + def action_sync(self): + for record in self: + record._sync_groups() + + def action_disable(self): + for record in self: + record.group_ids.unlink() + + @property + def _server_env_fields(self): + return {"api_key": {}} diff --git a/cross_connect_client/models/res_groups.py b/cross_connect_client/models/res_groups.py new file mode 100644 index 0000000000..64c95f1bb9 --- /dev/null +++ b/cross_connect_client/models/res_groups.py @@ -0,0 +1,24 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResGroups(models.Model): + _inherit = "res.groups" + + cross_connect_server_id = fields.Many2one( + "cross.connect.server", string="Originating Cross Connect Server" + ) + cross_connect_server_group_id = fields.Integer( + string="Originating Cross Connect Server Group ID" + ) + + _sql_constraints = [ + ( + "cross_connect_server_group_id_cross_connect_server_id_unique", + "unique (cross_connect_server_group_id, cross_connect_server_id)", + "Cross Connect Server Group ID must be unique per Cross Connect Server", + ) + ] diff --git a/cross_connect_client/readme/CONTRIBUTORS.md b/cross_connect_client/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..328a37da87 --- /dev/null +++ b/cross_connect_client/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Florian Mounier diff --git a/cross_connect_client/readme/DESCRIPTION.md b/cross_connect_client/readme/DESCRIPTION.md new file mode 100644 index 0000000000..b1f95a72ac --- /dev/null +++ b/cross_connect_client/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module allows this odoo instance users to connect directly on another odoo instance +where the module `cross_connect_server` is installed. diff --git a/cross_connect_client/readme/USAGE.md b/cross_connect_client/readme/USAGE.md new file mode 100644 index 0000000000..e16221aff0 --- /dev/null +++ b/cross_connect_client/readme/USAGE.md @@ -0,0 +1,23 @@ +First of all after installing the module, you need to configure the server connection. + +In order to do that, you need to go to the menu +`Settings > Technical > Cross Connect > Cross Connect Servers` and create a new server +to connect to. + +Fill the fields with the server's information : + +- Url: The api root path (e.g. `https://my-remote-odoo.com/api`) +- Api Key: The api-key from the `cross_connect_server` configuration + +Then click on the `Sync Cross Connection` button to check if the connection is working +and to sync the remote server's groups. + +After that, you will have to affect the remote groups to the local users in order for +them to be able to connect to the remote server. + +Once an user has a remote group, a new top level menu will appear in the menu bar with +the Cross Connect Server's name. Clicking on it will redirect the user to the remote +server logged in as the user. + +You can change each menu icon (for use with `web_responsive` for instance) by setting +the `Web Icon Data` in the server configuration. diff --git a/cross_connect_client/security/ir_model_access.xml b/cross_connect_client/security/ir_model_access.xml new file mode 100644 index 0000000000..56e3740c14 --- /dev/null +++ b/cross_connect_client/security/ir_model_access.xml @@ -0,0 +1,17 @@ + + + + + Cross Connect Server: Manager RW access + + + + + + + + diff --git a/cross_connect_client/static/description/index.html b/cross_connect_client/static/description/index.html new file mode 100644 index 0000000000..91e8634a6e --- /dev/null +++ b/cross_connect_client/static/description/index.html @@ -0,0 +1,450 @@ + + + + + +Cross Connect Client + + + +
+

Cross Connect Client

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runboat

+

This module allows this odoo instance users to connect directly on +another odoo instance where the module cross_connect_server is +installed.

+

Table of contents

+ +
+

Usage

+

First of all after installing the module, you need to configure the +server connection.

+

In order to do that, you need to go to the menu +Settings > Technical > Cross Connect > Cross Connect Servers and +create a new server to connect to.

+

Fill the fields with the server’s information :

+
    +
  • Url: The api root path (e.g. https://my-remote-odoo.com/api)
  • +
  • Api Key: The api-key from the cross_connect_server configuration
  • +
+

Then click on the Sync Cross Connection button to check if the +connection is working and to sync the remote server’s groups.

+

After that, you will have to affect the remote groups to the local users +in order for them to be able to connect to the remote server.

+

Once an user has a remote group, a new top level menu will appear in the +menu bar with the Cross Connect Server’s name. Clicking on it will +redirect the user to the remote server logged in as the user.

+

You can change each menu icon (for use with web_responsive for +instance) by setting the Web Icon Data in the server configuration.

+
+
+

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

+
    +
  • Akretion
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainer:

+

paradoxxxzero

+

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

+

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

+
+
+
+ + diff --git a/cross_connect_client/static/description/web_icon_data.png b/cross_connect_client/static/description/web_icon_data.png new file mode 100644 index 0000000000..db41199ed9 Binary files /dev/null and b/cross_connect_client/static/description/web_icon_data.png differ diff --git a/cross_connect_client/tests/__init__.py b/cross_connect_client/tests/__init__.py new file mode 100644 index 0000000000..a45b854598 --- /dev/null +++ b/cross_connect_client/tests/__init__.py @@ -0,0 +1 @@ +from . import test_cross_connect_client diff --git a/cross_connect_client/tests/test_cross_connect_client.py b/cross_connect_client/tests/test_cross_connect_client.py new file mode 100644 index 0000000000..1f944522c6 --- /dev/null +++ b/cross_connect_client/tests/test_cross_connect_client.py @@ -0,0 +1,276 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from unittest.mock import Mock, patch + +from odoo.exceptions import UserError +from odoo.tests.common import HttpCase, TransactionCase + + +def _mock_json(data): + res = Mock() + res.json = lambda: data + return res + + +class TestCrossConnectClient(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.server = cls.env["cross.connect.server"].create( + { + "name": "Test Server", + "server_url": "http://test-server.example.com", + "api_key": "server-api-key", + } + ) + cls.env["cross.connect.server"].create( + { + "name": "Other Test Server", + "server_url": "http://other-test-server.example.com", + "api_key": "other-server-api-key", + } + ) + + def test_base(self): + self.assertFalse(self.server.group_ids) + self.assertFalse(self.server.menu_id) + self.assertFalse(self.server.web_icon_data) + + def test_name(self): + self.server.name = False + self.server.server_url = "//[" + self.assertEqual(self.server.name, "/") + self.server.server_url = "https://test.example.org" + self.assertEqual(self.server.name, "test.example.org") + + def test_absolute_url_for(self): + self.assertEqual( + self.server._absolute_url_for("test"), + "http://test-server.example.com/cross_connect/test", + ) + + self.assertEqual( + self.server._absolute_url_for("/test"), + "http://test-server.example.com/cross_connect/test", + ) + + self.server.server_url = "http://test-server.example.com/" + + self.assertEqual( + self.server._absolute_url_for("test"), + "http://test-server.example.com/cross_connect/test", + ) + + self.assertEqual( + self.server._absolute_url_for("/test"), + "http://test-server.example.com/cross_connect/test", + ) + + def test_menu(self): + self.assertFalse(self.server.menu_id) + self.server.group_ids = [(0, 0, {"name": "Test Group"})] + self.assertTrue(self.server.menu_id) + self.server.web_icon_data = b"YQ==" + self.assertEqual(self.server.menu_id.web_icon_data, b"YQ==") + self.server.action_disable() + self.assertFalse(self.server.menu_id) + + @patch("requests.request") + def test_sync(self, req): + req.return_value = _mock_json( + { + "groups": [ + {"id": 1, "name": "Test Group 1", "comment": "Comment 1"}, + {"id": 2, "name": "Test Group 2", "comment": "Comment 2"}, + ] + } + ) + + self.server.action_sync() + + req.assert_called_once_with( + "GET", + "http://test-server.example.com/cross_connect/sync", + headers={"api-key": "server-api-key"}, + json=None, + timeout=10, + ) + + self.assertEqual(len(self.server.group_ids), 2) + self.assertEqual(self.server.group_ids[0].name, "Test Server: Test Group 1") + self.assertEqual(self.server.group_ids[0].comment, "Comment 1") + self.assertEqual(self.server.group_ids[0].cross_connect_server_group_id, 1) + + self.assertEqual(self.server.group_ids[1].name, "Test Server: Test Group 2") + self.assertEqual(self.server.group_ids[1].comment, "Comment 2") + self.assertEqual(self.server.group_ids[1].cross_connect_server_group_id, 2) + + self.assertTrue(self.server.menu_id) + self.assertEqual(self.server.menu_id.name, "Test Server") + self.assertEqual( + self.server.menu_id.web_icon, + "cross_connect_client,static/description/web_icon_data.png", + ) + self.assertEqual( + self.server.menu_id.groups_id, + self.server.group_ids | self.env.ref("base.group_system"), + ) + self.assertTrue(self.server.menu_id.action.name, "Test Server") + self.assertEqual( + self.server.menu_id.action.url, f"/cross_connect_server/{self.server.id}" + ) + self.assertEqual(self.server.menu_id.action.target, "self") + + self.assertTrue(self.server.web_icon_data) + + @patch("requests.request") + def test_successive_sync(self, req): + req.return_value = _mock_json( + { + "groups": [ + {"id": 1, "name": "Test Group 1", "comment": "Comment 1"}, + {"id": 2, "name": "Test Group 2", "comment": "Comment 2"}, + ] + } + ) + + self.server.action_sync() + req.return_value = _mock_json( + { + "groups": [ + { + "id": 2, + "name": "Test Group 2 edited", + "comment": "Comment edited 2", + }, + {"id": 3, "name": "Test Group 3", "comment": "Comment 3"}, + ] + } + ) + + self.server.action_sync() + + self.assertEqual(len(self.server.group_ids), 2) + self.assertEqual( + self.server.group_ids[0].name, "Test Server: Test Group 2 edited" + ) + self.assertEqual(self.server.group_ids[0].comment, "Comment edited 2") + self.assertEqual(self.server.group_ids[0].cross_connect_server_group_id, 2) + + self.assertEqual(self.server.group_ids[1].name, "Test Server: Test Group 3") + self.assertEqual(self.server.group_ids[1].comment, "Comment 3") + self.assertEqual(self.server.group_ids[1].cross_connect_server_group_id, 3) + + self.assertTrue(self.server.menu_id) + self.assertTrue(self.server.web_icon_data) + + @patch("requests.request") + def test_get_cross_connect_url(self, req): + req.return_value = _mock_json( + { + "groups": [ + {"id": 1, "name": "Test Group 1", "comment": "Comment 1"}, + {"id": 2, "name": "Test Group 2", "comment": "Comment 2"}, + ] + } + ) + self.server.action_sync() + + user = self.env["res.users"].create({"name": "Test User", "login": "test_user"}) + group = self.server.group_ids[0] + user.write({"groups_id": [(4, group.id)]}) + + req.reset_mock() + req.return_value = _mock_json({"client_id": 1, "token": "test-token"}) + + self.assertEqual( + self.server.with_user(user).sudo()._get_cross_connect_url(), + "http://test-server.example.com/cross_connect/login/1/test-token", + ) + + req.assert_called_once_with( + "POST", + "http://test-server.example.com/cross_connect/access", + headers={"api-key": "server-api-key"}, + json={ + "name": "Test User", + "login": "test_user", + "lang": "en_US", + "groups": [group.cross_connect_server_group_id], + }, + timeout=10, + ) + req.reset_mock() + req.return_value = _mock_json({"client_id": 1}) + with self.assertRaisesRegex(UserError, "Missing token"): + self.server.with_user(user).sudo()._get_cross_connect_url() + + @patch("requests.request") + def test_get_cross_connect_url_bad_groups(self, req): + req.return_value = _mock_json( + { + "groups": [ + {"id": 1, "name": "Test Group 1", "comment": "Comment 1"}, + {"id": 2, "name": "Test Group 2", "comment": "Comment 2"}, + ] + } + ) + self.server.action_sync() + + user = self.env["res.users"].create({"name": "Test User", "login": "test_user"}) + + req.reset_mock() + req.return_value = _mock_json({"client_id": 1, "token": "test-token"}) + + with self.assertRaisesRegex( + UserError, "You are not allowed to access this server" + ): + self.server.with_user(user).sudo()._get_cross_connect_url() + + +class TestCrossConnectClientController(HttpCase): + def test_act_url_redirect(self): + server = self.env["cross.connect.server"].create( + { + "name": "Test Server", + "server_url": "http://test-server.example.com", + "api_key": "server-api-key", + } + ) + with patch( + "requests.request", + return_value=_mock_json( + { + "groups": [ + {"id": 1, "name": "Test Group 1", "comment": "Comment 1"}, + {"id": 2, "name": "Test Group 2", "comment": "Comment 2"}, + ] + } + ), + ): + server.action_sync() + + user = self.env["res.users"].create( + {"name": "Test User", "login": "test_user", "password": "user_pas$w0rd"} + ) + group = server.group_ids[0] + user.write({"groups_id": [(4, group.id)]}) + self.authenticate("test_user", "user_pas$w0rd") + with patch( + "requests.request", + return_value=_mock_json({"client_id": 1, "token": "test-token"}), + ): + resp = self.url_open(server.menu_id.action.url, allow_redirects=False) + resp.raise_for_status() + self.assertEqual(resp.status_code, 303) + self.assertEqual( + resp.headers["Location"], + "http://test-server.example.com/cross_connect/login/1/test-token", + ) + + def test_bad_server(self): + self.assertFalse(self.env["cross.connect.server"].search([])) + resp = self.url_open("/cross_connect_server/1", allow_redirects=False) + self.assertEqual(resp.status_code, 400) diff --git a/cross_connect_client/views/cross_connect_server_views.xml b/cross_connect_client/views/cross_connect_server_views.xml new file mode 100644 index 0000000000..9c317a1220 --- /dev/null +++ b/cross_connect_client/views/cross_connect_server_views.xml @@ -0,0 +1,93 @@ + + + + + Cross Connect Servers + cross.connect.server + tree,form + + + + cross.connect.server.form + cross.connect.server + +
+
+
+ + +
+

+ +

+
+ + + + + + + + + +
+
+
+
+ + + cross.connect.server.tree + cross.connect.server + + + + + + + + + + + +
diff --git a/setup/cross_connect_client/odoo/addons/cross_connect_client b/setup/cross_connect_client/odoo/addons/cross_connect_client new file mode 120000 index 0000000000..9142e33086 --- /dev/null +++ b/setup/cross_connect_client/odoo/addons/cross_connect_client @@ -0,0 +1 @@ +../../../../cross_connect_client \ No newline at end of file diff --git a/setup/cross_connect_client/setup.py b/setup/cross_connect_client/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/cross_connect_client/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)