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..5a406903
--- /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.esm";
+
+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 @@
+
+
+
+
+
+
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)