From 36e786f4630fb2a14cae228b050074444e3efac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20TA=C5=9ECI?= Date: Tue, 4 Aug 2026 17:02:57 +0300 Subject: [PATCH 1/2] [ADD] spreadsheet_dashboard_share_oca: manage dashboard share links --- spreadsheet_dashboard_share_oca/README.rst | 9 ++ spreadsheet_dashboard_share_oca/__init__.py | 1 + .../__manifest__.py | 23 +++ .../i18n/spreadsheet_dashboard_share_oca.pot | 98 ++++++++++++ .../models/__init__.py | 1 + .../models/spreadsheet_dashboard_share.py | 39 +++++ .../pyproject.toml | 3 + .../readme/CONFIGURATION.md | 3 + .../readme/SUMMARY.md | 3 + .../readme/USAGE.md | 7 + .../security/ir_rule.xml | 12 ++ .../static/src/bundle/dashboard_badge.esm.js | 34 ++++ .../static/src/bundle/dashboard_badge.xml | 29 ++++ .../static/src/bundle/share_button.xml | 15 ++ .../src/bundle/share_button_patch.esm.js | 30 ++++ .../share_manage/share_manage_dialog.esm.js | 48 ++++++ .../share_manage/share_manage_dialog.xml | 53 +++++++ .../static/tests/share_manage_dialog.test.js | 79 ++++++++++ .../tests/__init__.py | 1 + .../tests/test_share_management.py | 146 ++++++++++++++++++ 20 files changed, 634 insertions(+) create mode 100644 spreadsheet_dashboard_share_oca/README.rst create mode 100644 spreadsheet_dashboard_share_oca/__init__.py create mode 100644 spreadsheet_dashboard_share_oca/__manifest__.py create mode 100644 spreadsheet_dashboard_share_oca/i18n/spreadsheet_dashboard_share_oca.pot create mode 100644 spreadsheet_dashboard_share_oca/models/__init__.py create mode 100644 spreadsheet_dashboard_share_oca/models/spreadsheet_dashboard_share.py create mode 100644 spreadsheet_dashboard_share_oca/pyproject.toml create mode 100644 spreadsheet_dashboard_share_oca/readme/CONFIGURATION.md create mode 100644 spreadsheet_dashboard_share_oca/readme/SUMMARY.md create mode 100644 spreadsheet_dashboard_share_oca/readme/USAGE.md create mode 100644 spreadsheet_dashboard_share_oca/security/ir_rule.xml create mode 100644 spreadsheet_dashboard_share_oca/static/src/bundle/dashboard_badge.esm.js create mode 100644 spreadsheet_dashboard_share_oca/static/src/bundle/dashboard_badge.xml create mode 100644 spreadsheet_dashboard_share_oca/static/src/bundle/share_button.xml create mode 100644 spreadsheet_dashboard_share_oca/static/src/bundle/share_button_patch.esm.js create mode 100644 spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.esm.js create mode 100644 spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml create mode 100644 spreadsheet_dashboard_share_oca/static/tests/share_manage_dialog.test.js create mode 100644 spreadsheet_dashboard_share_oca/tests/__init__.py create mode 100644 spreadsheet_dashboard_share_oca/tests/test_share_management.py diff --git a/spreadsheet_dashboard_share_oca/README.rst b/spreadsheet_dashboard_share_oca/README.rst new file mode 100644 index 00000000..221b42a0 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/README.rst @@ -0,0 +1,9 @@ +============================= +Spreadsheet Dashboard Share +============================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + This file is generated by oca-gen-addon-readme from + the readme fragments; do not edit it directly. + +.. automodule:: spreadsheet_dashboard_share_oca diff --git a/spreadsheet_dashboard_share_oca/__init__.py b/spreadsheet_dashboard_share_oca/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/spreadsheet_dashboard_share_oca/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/spreadsheet_dashboard_share_oca/__manifest__.py b/spreadsheet_dashboard_share_oca/__manifest__.py new file mode 100644 index 00000000..495f6758 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2026 Volkan Tasci +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Spreadsheet Dashboard Share OCA", + "summary": "Manage dashboard share links", + "version": "19.0.1.0.0", + "license": "AGPL-3", + "author": "Volkan Tasci,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/spreadsheet", + "depends": ["spreadsheet_dashboard"], + "data": ["security/ir_rule.xml"], + "assets": { + "spreadsheet.o_spreadsheet": [ + "spreadsheet_dashboard_share_oca/static/src/bundle/**/*.js", + "spreadsheet_dashboard_share_oca/static/src/bundle/**/*.xml", + ], + "web.assets_unit_tests": [ + "spreadsheet_dashboard_share_oca/static/tests/**/*", + ], + }, + "maintainers": ["volkantasci"], +} diff --git a/spreadsheet_dashboard_share_oca/i18n/spreadsheet_dashboard_share_oca.pot b/spreadsheet_dashboard_share_oca/i18n/spreadsheet_dashboard_share_oca.pot new file mode 100644 index 00000000..ed92fc8a --- /dev/null +++ b/spreadsheet_dashboard_share_oca/i18n/spreadsheet_dashboard_share_oca.pot @@ -0,0 +1,98 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * spreadsheet_dashboard_share_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0-20260723\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-04 14:15+0000\n" +"PO-Revision-Date: 2026-08-04 14:15+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml:0 +msgid "Actions" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/dashboard_badge.xml:0 +msgid "Active shares" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml:0 +msgid "Close" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.esm.js:0 +msgid "Copied" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#: model:ir.model,name:spreadsheet_dashboard_share_oca.model_spreadsheet_dashboard_share +msgid "Copy of a shared dashboard" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml:0 +msgid "Created by" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml:0 +msgid "Created on" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml:0 +msgid "Dashboard" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_share_oca.field_spreadsheet_dashboard_share__display_name +msgid "Display Name" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#: model:ir.model.fields,field_description:spreadsheet_dashboard_share_oca.field_spreadsheet_dashboard_share__id +msgid "ID" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_button.xml:0 +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_button_patch.esm.js:0 +msgid "Manage shares" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml:0 +msgid "No shares yet." +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml:0 +msgid "Revoke" +msgstr "" + +#. module: spreadsheet_dashboard_share_oca +#. odoo-javascript +#: code:addons/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml:0 +msgid "Share link" +msgstr "" diff --git a/spreadsheet_dashboard_share_oca/models/__init__.py b/spreadsheet_dashboard_share_oca/models/__init__.py new file mode 100644 index 00000000..6a1d1be6 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/models/__init__.py @@ -0,0 +1 @@ +from . import spreadsheet_dashboard_share diff --git a/spreadsheet_dashboard_share_oca/models/spreadsheet_dashboard_share.py b/spreadsheet_dashboard_share_oca/models/spreadsheet_dashboard_share.py new file mode 100644 index 00000000..d0ffdecb --- /dev/null +++ b/spreadsheet_dashboard_share_oca/models/spreadsheet_dashboard_share.py @@ -0,0 +1,39 @@ +# Copyright 2026 Volkan Tasci +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class SpreadsheetDashboardShare(models.Model): + _inherit = "spreadsheet.dashboard.share" + + @api.model + def action_get_dashboard_shares(self, dashboard_id): + """Return the shares of a dashboard the current user can see.""" + shares = self.search([("dashboard_id", "=", dashboard_id)]) + return [ + { + "id": share.id, + "full_url": share.full_url, + "create_date": ( + share.create_date.isoformat() if share.create_date else False + ), + "create_uid": ( + share.create_uid.display_name if share.create_uid else False + ), + "name": share.name, + } + for share in shares + ] + + @api.model + def action_unshare(self, share_ids): + """Revoke shares. ir.rule limits the user to the shares they may access.""" + self.browse(share_ids).unlink() + return True + + @api.model + def action_get_share_counts(self): + """Return {dashboard_id: share_count} for shares visible to the user.""" + counts = self._read_group([], ["dashboard_id"], ["__count"]) + return {count[0].id: count[1] for count in counts if count[0]} diff --git a/spreadsheet_dashboard_share_oca/pyproject.toml b/spreadsheet_dashboard_share_oca/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/spreadsheet_dashboard_share_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spreadsheet_dashboard_share_oca/readme/CONFIGURATION.md b/spreadsheet_dashboard_share_oca/readme/CONFIGURATION.md new file mode 100644 index 00000000..c80daf35 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/readme/CONFIGURATION.md @@ -0,0 +1,3 @@ +Users can only see and revoke the shares they created. Users in the +"Dashboards / Admin" group (spreadsheet_dashboard.group_dashboard_manager) +can see and revoke the shares created by any user. diff --git a/spreadsheet_dashboard_share_oca/readme/SUMMARY.md b/spreadsheet_dashboard_share_oca/readme/SUMMARY.md new file mode 100644 index 00000000..4fd768b7 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/readme/SUMMARY.md @@ -0,0 +1,3 @@ +This module allows users to manage the public share links of their +spreadsheet dashboards: list, copy, and revoke them. Dashboard Admins can +manage the shares created by any user. diff --git a/spreadsheet_dashboard_share_oca/readme/USAGE.md b/spreadsheet_dashboard_share_oca/readme/USAGE.md new file mode 100644 index 00000000..0b40eae1 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/readme/USAGE.md @@ -0,0 +1,7 @@ +- Open the "Dashboards" app (Rapor Panelleri). +- Open a dashboard and click on the **Share** button. +- Click on **Manage shares** to list every share link of the dashboard. +- Use the copy button to copy a link, or click **Revoke** to make a link + return a 404 error. +- A badge next to the dashboard name in the sidebar shows the number of + active shares. diff --git a/spreadsheet_dashboard_share_oca/security/ir_rule.xml b/spreadsheet_dashboard_share_oca/security/ir_rule.xml new file mode 100644 index 00000000..c891e454 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/security/ir_rule.xml @@ -0,0 +1,12 @@ + + + + spreadsheet.dashboard.share: manager sees all + + + [(1, '=', 1)] + + diff --git a/spreadsheet_dashboard_share_oca/static/src/bundle/dashboard_badge.esm.js b/spreadsheet_dashboard_share_oca/static/src/bundle/dashboard_badge.esm.js new file mode 100644 index 00000000..40e53c68 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/static/src/bundle/dashboard_badge.esm.js @@ -0,0 +1,34 @@ +import {onWillStart, useState} from "@odoo/owl"; +import {SpreadsheetDashboardAction} from "@spreadsheet_dashboard/bundle/dashboard_action/dashboard_action"; +import {patch} from "@web/core/utils/patch"; +import {useBus} from "@web/core/utils/hooks"; + +patch(SpreadsheetDashboardAction.prototype, { + setup() { + super.setup(); + this.shareCounts = useState({}); + onWillStart(async () => { + await this._loadShareCounts(); + }); + useBus(this.env.bus, "dashboards-shares-updated", () => + this._loadShareCounts() + ); + }, + async _loadShareCounts() { + try { + const counts = await this.orm.call( + "spreadsheet.dashboard.share", + "action_get_share_counts", + [] + ); + Object.assign(this.shareCounts, counts); + } catch { + // Keep previous counts on failure + } + }, + async shareSpreadsheet(data, excelExport) { + const url = await super.shareSpreadsheet(data, excelExport); + this.env.bus.trigger("dashboards-shares-updated"); + return url; + }, +}); diff --git a/spreadsheet_dashboard_share_oca/static/src/bundle/dashboard_badge.xml b/spreadsheet_dashboard_share_oca/static/src/bundle/dashboard_badge.xml new file mode 100644 index 00000000..136565a6 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/static/src/bundle/dashboard_badge.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/spreadsheet_dashboard_share_oca/static/src/bundle/share_button.xml b/spreadsheet_dashboard_share_oca/static/src/bundle/share_button.xml new file mode 100644 index 00000000..df9d4613 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/static/src/bundle/share_button.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/spreadsheet_dashboard_share_oca/static/src/bundle/share_button_patch.esm.js b/spreadsheet_dashboard_share_oca/static/src/bundle/share_button_patch.esm.js new file mode 100644 index 00000000..31ff0cd8 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/static/src/bundle/share_button_patch.esm.js @@ -0,0 +1,30 @@ +import {SpreadsheetShareButton} from "@spreadsheet/components/share_button/share_button"; +import {_t} from "@web/core/l10n/translation"; +import {patch} from "@web/core/utils/patch"; +import {useService} from "@web/core/utils/hooks"; + +import {ShareManageDialog} from "./share_manage/share_manage_dialog"; + +const MANAGE_SHARES = _t("Manage shares"); + +patch(SpreadsheetShareButton.prototype, { + setup() { + super.setup(); + this.dialog = useService("dialog"); + this.isDashboard = Boolean(this.env.services.spreadsheet_dashboard_loader); + }, + onManageShares() { + if (!this.isDashboard) { + return; + } + const dashboard = + this.env.services.spreadsheet_dashboard_loader.getActiveDashboard(); + if (!dashboard) { + return; + } + this.dialog.add(ShareManageDialog, { + dashboardId: dashboard.data.id, + title: `${dashboard.data.name} - ${MANAGE_SHARES}`, + }); + }, +}); diff --git a/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.esm.js b/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.esm.js new file mode 100644 index 00000000..5ff1e07e --- /dev/null +++ b/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.esm.js @@ -0,0 +1,48 @@ +import {Component, onWillStart, useState} from "@odoo/owl"; +import {CopyButton} from "@web/core/copy_button/copy_button"; +import {Dialog} from "@web/core/dialog/dialog"; +import {_t} from "@web/core/l10n/translation"; +import {useService} from "@web/core/utils/hooks"; + +export class ShareManageDialog extends Component { + static template = "spreadsheet_dashboard_share_oca.ShareManageDialog"; + static components = {CopyButton, Dialog}; + static props = { + dashboardId: {type: Number}, + title: {type: String}, + close: {type: Function, optional: true}, + }; + + setup() { + this.copiedText = _t("Copied"); + this.orm = useService("orm"); + this.state = useState({shares: [], revoking: 0}); + onWillStart(async () => { + await this._loadShares(); + }); + } + + async _loadShares() { + this.state.shares = await this.orm.call( + "spreadsheet.dashboard.share", + "action_get_dashboard_shares", + [this.props.dashboardId] + ); + } + + async onRevoke(share) { + if (this.state.revoking === share.id) { + return; + } + this.state.revoking = share.id; + try { + await this.orm.call("spreadsheet.dashboard.share", "action_unshare", [ + [share.id], + ]); + this.state.shares = this.state.shares.filter((s) => s.id !== share.id); + this.env.bus.trigger("dashboards-shares-updated"); + } finally { + this.state.revoking = 0; + } + } +} diff --git a/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml b/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml new file mode 100644 index 00000000..4bcc9042 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/static/src/bundle/share_manage/share_manage_dialog.xml @@ -0,0 +1,53 @@ + + + + +

+ No shares yet. +

+ + + + + + + + + + + + + + + + +
DashboardShare linkCreated byCreated onActions
+ +
+ + +
+
+ + + +
+ + + +
+
+
diff --git a/spreadsheet_dashboard_share_oca/static/tests/share_manage_dialog.test.js b/spreadsheet_dashboard_share_oca/static/tests/share_manage_dialog.test.js new file mode 100644 index 00000000..8ce57e97 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/static/tests/share_manage_dialog.test.js @@ -0,0 +1,79 @@ +import { + contains, + makeDialogMockEnv, + mountWithCleanup, + patchWithCleanup, +} from "@web/../tests/web_test_helpers"; +import {describe, expect, test} from "@odoo/hoot"; + +import {ShareManageDialog} from "@spreadsheet_dashboard_share_oca/bundle/share_manage/share_manage_dialog"; + +describe.current.tags("desktop"); + +const SHARES = [ + { + id: 1, + full_url: "http://localhost/dashboard/share/1/token1", + create_date: "2026-08-04T10:00:00", + create_uid: "Raoul", + name: "DeepSeek API Giderleri", + }, + { + id: 2, + full_url: "http://localhost/dashboard/share/2/token2", + create_date: "2026-08-04T11:00:00", + create_uid: "Bob", + name: "DeepSeek API Giderleri", + }, +]; + +async function mountDialog(env, {shares = SHARES} = {}) { + patchWithCleanup(env.services.orm, { + async call(model, method) { + if (method === "action_get_dashboard_shares") { + return shares; + } + if (method === "action_unshare") { + return true; + } + throw new Error(`Unexpected method: ${method}`); + }, + }); + await mountWithCleanup(ShareManageDialog, { + env, + props: { + dashboardId: 3, + title: "DeepSeek API Giderleri - Manage shares", + }, + }); +} + +test("renders the share list", async () => { + const env = await makeDialogMockEnv(); + await mountDialog(env); + expect(".modal-header").toHaveText(/Manage shares/); + expect("tbody tr").toHaveCount(2); + expect("tbody tr").toHaveText(/Raoul/); + expect("tbody tr").toHaveText(/Bob/); + expect("tbody tr").toHaveText(/dashboard\/share\/1\/token1/); +}); + +test("shows empty state when there are no shares", async () => { + const env = await makeDialogMockEnv(); + await mountDialog(env, {shares: []}); + expect("tbody").toHaveCount(0); + expect("body").toHaveText(/No shares yet/); +}); + +test("revoke removes the row and notifies the badge", async () => { + const env = await makeDialogMockEnv(); + let busEvents = 0; + env.bus.addEventListener("dashboards-shares-updated", () => busEvents++); + await mountDialog(env); + expect("tbody tr").toHaveCount(2); + await contains(".btn-danger").click(); + expect("tbody tr").toHaveCount(1); + expect("tbody tr").toHaveText(/Bob/); + expect("tbody tr").toHaveText(/dashboard\/share\/2\/token2/); + expect(busEvents).toBe(1); +}); diff --git a/spreadsheet_dashboard_share_oca/tests/__init__.py b/spreadsheet_dashboard_share_oca/tests/__init__.py new file mode 100644 index 00000000..538fcda5 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/tests/__init__.py @@ -0,0 +1 @@ +from . import test_share_management diff --git a/spreadsheet_dashboard_share_oca/tests/test_share_management.py b/spreadsheet_dashboard_share_oca/tests/test_share_management.py new file mode 100644 index 00000000..495a48b7 --- /dev/null +++ b/spreadsheet_dashboard_share_oca/tests/test_share_management.py @@ -0,0 +1,146 @@ +# Copyright 2026 Volkan Tasci +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.exceptions import AccessError +from odoo.tests import HttpCase, new_test_user + +from odoo.addons.spreadsheet_dashboard.tests.common import DashboardTestCommon + + +class TestShareManagement(DashboardTestCommon, HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.user2 = new_test_user(cls.env, login="Bob") + cls.user2.group_ids |= cls.group + cls.manager = new_test_user( + cls.env, + login="Manager", + groups="spreadsheet_dashboard.group_dashboard_manager", + ) + + def _share_as(self, user, dashboard): + return ( + self.env["spreadsheet.dashboard.share"] + .with_user(user) + .create( + { + "dashboard_id": dashboard.id, + "spreadsheet_data": dashboard.spreadsheet_data, + } + ) + ) + + def test_list_only_own_shares(self): + dashboard = self.create_dashboard() + share_raoul = self._share_as(self.user, dashboard) + share_bob = self._share_as(self.user2, dashboard) + + shares = ( + self.env["spreadsheet.dashboard.share"] + .with_user(self.user) + .action_get_dashboard_shares(dashboard.id) + ) + self.assertEqual(len(shares), 1) + self.assertEqual(shares[0]["id"], share_raoul.id) + self.assertEqual(shares[0]["full_url"], share_raoul.full_url) + self.assertEqual(shares[0]["name"], dashboard.name) + self.assertEqual(shares[0]["create_uid"], self.user.display_name) + self.assertTrue(shares[0]["create_date"]) + self.assertNotEqual(shares[0]["id"], share_bob.id) + + def test_manager_sees_all_shares(self): + dashboard = self.create_dashboard() + share_raoul = self._share_as(self.user, dashboard) + share_bob = self._share_as(self.user2, dashboard) + + shares = ( + self.env["spreadsheet.dashboard.share"] + .with_user(self.manager) + .action_get_dashboard_shares(dashboard.id) + ) + self.assertEqual( + {share["id"] for share in shares}, {share_raoul.id, share_bob.id} + ) + + def test_unshare_own_share(self): + dashboard = self.create_dashboard() + share = self._share_as(self.user, dashboard) + + result = ( + self.env["spreadsheet.dashboard.share"] + .with_user(self.user) + .action_unshare([share.id]) + ) + self.assertTrue(result) + self.assertFalse( + self.env["spreadsheet.dashboard.share"].search([("id", "=", share.id)]) + ) + + def test_cannot_unshare_other_users_share(self): + dashboard = self.create_dashboard() + share = self._share_as(self.user2, dashboard) + + with self.assertRaises(AccessError): + ( + self.env["spreadsheet.dashboard.share"] + .with_user(self.user) + .action_unshare([share.id]) + ) + + def test_manager_can_unshare_other_users_share(self): + dashboard = self.create_dashboard() + share = self._share_as(self.user2, dashboard) + + result = ( + self.env["spreadsheet.dashboard.share"] + .with_user(self.manager) + .action_unshare([share.id]) + ) + self.assertTrue(result) + self.assertFalse( + self.env["spreadsheet.dashboard.share"].search([("id", "=", share.id)]) + ) + + def test_unshared_link_returns_404(self): + dashboard = self.create_dashboard() + share = self._share_as(self.user, dashboard) + access_token = share.access_token + share.with_user(self.user).action_unshare([share.id]) + + response = self.url_open(f"/dashboard/share/{share.id}/{access_token}") + self.assertEqual(response.status_code, 404) + + def test_share_counts_own_shares_only(self): + dashboard = self.create_dashboard() + self._share_as(self.user, dashboard) + self._share_as(self.user2, dashboard) + + counts = ( + self.env["spreadsheet.dashboard.share"] + .with_user(self.user) + .action_get_share_counts() + ) + self.assertEqual(counts[dashboard.id], 1) + + def test_share_counts_manager_sees_all(self): + dashboard = self.create_dashboard() + self._share_as(self.user, dashboard) + self._share_as(self.user2, dashboard) + + counts = ( + self.env["spreadsheet.dashboard.share"] + .with_user(self.manager) + .action_get_share_counts() + ) + self.assertEqual(counts[dashboard.id], 2) + + def test_share_counts_empty_for_dashboard_without_shares(self): + dashboard = self.create_dashboard() + + counts = ( + self.env["spreadsheet.dashboard.share"] + .with_user(self.manager) + .action_get_share_counts() + ) + self.assertNotIn(dashboard.id, counts) From 62beb16d293cb6f157f3b330dce5ee3510477a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20TA=C5=9ECI?= Date: Wed, 5 Aug 2026 03:01:22 +0300 Subject: [PATCH 2/2] fix: import bundle modules with their .esm suffix The o_spreadsheet bundle compiler derives a module id from the file path by stripping only the trailing '.js' extension, so a file named share_manage_dialog.esm.js is registered under '.../share_manage_dialog.esm'. The import in share_button_patch referenced the module without the '.esm' suffix, so the dialog was never found ('module not defined' in the browser console) and the Manage shares button never appeared. Import the module with its '.esm' suffix, matching the pattern used by the other bundle modules of this repo (e.g. spreadsheet_oca). --- .../static/src/bundle/share_button_patch.esm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spreadsheet_dashboard_share_oca/static/src/bundle/share_button_patch.esm.js b/spreadsheet_dashboard_share_oca/static/src/bundle/share_button_patch.esm.js index 31ff0cd8..5a406903 100644 --- a/spreadsheet_dashboard_share_oca/static/src/bundle/share_button_patch.esm.js +++ b/spreadsheet_dashboard_share_oca/static/src/bundle/share_button_patch.esm.js @@ -3,7 +3,7 @@ import {_t} from "@web/core/l10n/translation"; import {patch} from "@web/core/utils/patch"; import {useService} from "@web/core/utils/hooks"; -import {ShareManageDialog} from "./share_manage/share_manage_dialog"; +import {ShareManageDialog} from "./share_manage/share_manage_dialog.esm"; const MANAGE_SHARES = _t("Manage shares");