From fee7446704c0549f93fa9ee88a943c94bfa62659 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Sat, 20 Jun 2026 08:58:04 -0400 Subject: [PATCH] [FIX] document_page_reference: migrate widget to OWL for Odoo 16 content_parsed is sanitize=False so the rendered oe_direct_line anchors the widget binds to survive (html_sanitize strips the data-oe-* attrs); the source content is still sanitized and page names are html_escape'd. --- document_page_reference/__manifest__.py | 3 + .../models/document_page.py | 54 +++++++++--- .../static/src/js/editor.esm.js | 42 ++++++++++ .../static/src/js/editor.js | 34 -------- .../test_document_page_reference_tour.esm.js | 84 +++++++++++++++++++ document_page_reference/tests/__init__.py | 1 + .../tests/test_document_reference.py | 39 ++++++++- .../tests/test_document_reference_tour.py | 58 +++++++++++++ .../views/document_page.xml | 19 +++-- 9 files changed, 281 insertions(+), 53 deletions(-) create mode 100644 document_page_reference/static/src/js/editor.esm.js delete mode 100644 document_page_reference/static/src/js/editor.js create mode 100644 document_page_reference/static/tests/test_document_page_reference_tour.esm.js create mode 100644 document_page_reference/tests/test_document_reference_tour.py diff --git a/document_page_reference/__manifest__.py b/document_page_reference/__manifest__.py index dea07a92925..0d13672558f 100644 --- a/document_page_reference/__manifest__.py +++ b/document_page_reference/__manifest__.py @@ -18,6 +18,9 @@ "web.assets_backend": [ "document_page_reference/static/src/js/**/*", ], + "web.assets_tests": [ + "document_page_reference/static/tests/**/*", + ], }, "maintainers": ["etobella"], } diff --git a/document_page_reference/models/document_page.py b/document_page_reference/models/document_page.py index 5c5fd37e267..0def89dc9b3 100644 --- a/document_page_reference/models/document_page.py +++ b/document_page_reference/models/document_page.py @@ -3,6 +3,8 @@ import logging +from markupsafe import Markup + from odoo import _, api, fields, models, tools from odoo.exceptions import ValidationError from odoo.tools.misc import html_escape @@ -52,7 +54,36 @@ class DocumentPage(models.Model): reference = fields.Char( help="Used to find the document, it can contain letters, numbers and _" ) - content_parsed = fields.Html(compute="_compute_content_parsed") + # sanitize=False keeps the rendered oe_direct_line anchors + # (data-oe-model/-id) the widget binds to, which html_sanitize strips. + # Safe: the source ``content`` is still sanitized and page names are + # html_escape'd, so no unsanitized user input reaches this field. + content_parsed = fields.Html(compute="_compute_content_parsed", sanitize=False) + + def _get_page_index(self, link=True): + """Override to use oe_direct_line links compatible with the widget.""" + self.ensure_one() + index = [ + Markup("
  • ") + subpage._get_page_index() + Markup("
  • ") + for subpage in self.child_ids + ] + r = Markup("") + if link: + r = ( + Markup( + '' + ) + % ( + self._name, + self.id, + ) + + html_escape(self.name) + + Markup("") + ) + if index: + r += Markup("") + return r def get_formview_action(self, access_uid=None): res = super().get_formview_action(access_uid) @@ -60,17 +91,20 @@ def get_formview_action(self, access_uid=None): res["views"] = [(view_id, "form")] return res - @api.depends("history_head") + @api.depends("history_head", "type") def _compute_content_parsed(self): for record in self: - content = record.get_content() - if content == "

    " and self.content != "

    ": - _logger.error( - "Template from page with id = %s cannot be processed correctly" - % self.id - ) - content = self.content - record.content_parsed = content + if record.type == "category": + record.content_parsed = record.content + else: + content = record.get_content() + if content == "

    " and record.content != "

    ": + _logger.error( + "Template from page with id = %s cannot be " + "processed correctly" % record.id + ) + content = record.content + record.content_parsed = content @api.constrains("reference") def _check_reference(self): diff --git a/document_page_reference/static/src/js/editor.esm.js b/document_page_reference/static/src/js/editor.esm.js new file mode 100644 index 00000000000..98fbe7f3654 --- /dev/null +++ b/document_page_reference/static/src/js/editor.esm.js @@ -0,0 +1,42 @@ +/* @odoo-module */ + +import {HtmlField} from "@web_editor/js/backend/html_field"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; +import {onMounted, onPatched} from "@odoo/owl"; + +export class DocumentPageReferenceField extends HtmlField { + setup() { + super.setup(); + this.actionService = useService("action"); + this.orm = useService("orm"); + this._onClickDirectLink = this._onClickDirectLink.bind(this); + onMounted(() => this._bindLinks()); + onPatched(() => this._bindLinks()); + } + _bindLinks() { + const el = this.readonlyElementRef && this.readonlyElementRef.el; + if (!el) return; + // Remove target="_blank" from internal reference links + // (added by retargetLinks in HtmlField) + for (const link of el.querySelectorAll("a.oe_direct_line")) { + link.removeAttribute("target"); + link.removeAttribute("rel"); + link.removeEventListener("click", this._onClickDirectLink); + link.addEventListener("click", this._onClickDirectLink); + } + } + _onClickDirectLink(ev) { + ev.preventDefault(); + ev.stopPropagation(); + const target = ev.currentTarget; + const model = target.dataset.oeModel; + const id = parseInt(target.dataset.oeId, 10); + if (!model || !id) return; + this.orm.call(model, "get_formview_action", [[id]]).then((action) => { + this.actionService.doAction(action); + }); + } +} + +registry.category("fields").add("document_page_reference", DocumentPageReferenceField); diff --git a/document_page_reference/static/src/js/editor.js b/document_page_reference/static/src/js/editor.js deleted file mode 100644 index 852df084ca9..00000000000 --- a/document_page_reference/static/src/js/editor.js +++ /dev/null @@ -1,34 +0,0 @@ -odoo.define("document_page_reference.backend", function (require) { - "use strict"; - - var field_registry = require("web.field_registry"); - var FieldTextHtmlSimple = require("web_editor.field.html"); - var FieldDocumentPage = FieldTextHtmlSimple.extend({ - events: _.extend({}, FieldTextHtmlSimple.prototype.events, { - "click .oe_direct_line": "_onClickDirectLink", - }), - _onClickDirectLink: function (event) { - var self = this; - event.preventDefault(); - event.stopPropagation(); - var element = $(event.target).closest(".oe_direct_line")[0]; - var default_reference = element.name; - var model = $(event.target).data("oe-model"); - var id = $(event.target).data("oe-id"); - var context = this.record.getContext(this.recordParams); - if (default_reference) { - context.default_reference = default_reference; - } - this._rpc({ - model: model, - method: "get_formview_action", - args: [[parseInt(id, 10)]], - context: context, - }).then(function (action) { - self.trigger_up("do_action", {action: action}); - }); - }, - }); - field_registry.add("document_page_reference", FieldDocumentPage); - return FieldDocumentPage; -}); diff --git a/document_page_reference/static/tests/test_document_page_reference_tour.esm.js b/document_page_reference/static/tests/test_document_page_reference_tour.esm.js new file mode 100644 index 00000000000..ab44e7b88ca --- /dev/null +++ b/document_page_reference/static/tests/test_document_page_reference_tour.esm.js @@ -0,0 +1,84 @@ +/** @odoo-module */ + +import tour from "web_tour.tour"; + +/* + * Test 1: Reference widget renders ${ref} as clickable links. + */ +tour.register( + "document_page_reference_widget_tour", + { + test: true, + url: "/web#action=document_page.action_page", + }, + [ + { + content: "Open Test Ref Page 1", + trigger: '.o_data_cell[name="name"]:contains("Test Ref Page 1")', + run: "click", + }, + { + content: "Verify content_parsed renders reference as link", + trigger: + '.o_form_view .o_field_widget[name="content_parsed"] a.oe_direct_line', + timeout: 20000, + run: function () { + var link = this.$anchor[0]; + if (!link.dataset.oeModel || !link.dataset.oeId) { + throw new Error("Reference link missing data-oe-model/data-oe-id"); + } + if (link.getAttribute("target") === "_blank") { + throw new Error( + "Internal reference link should not have target=_blank" + ); + } + }, + }, + ] +); + +/* + * Test 2: Category page index uses oe_direct_line links. + */ +tour.register( + "document_page_reference_category_tour", + { + test: true, + url: "/web#action=document_page.action_page", + }, + [ + { + content: "Open Test Ref Page 1 to navigate to its category", + trigger: '.o_data_cell[name="name"]:contains("Test Ref Page 1")', + run: "click", + }, + { + content: "Navigate to category via parent_id link", + trigger: '.o_form_view .o_field_widget[name="parent_id"] a', + timeout: 20000, + run: "click", + }, + { + content: "Verify category shows child links as oe_direct_line", + trigger: + '.o_form_view .o_field_widget[name="content_parsed"] a.oe_direct_line', + timeout: 20000, + run: function () { + var links = document.querySelectorAll( + '.o_field_widget[name="content_parsed"] a.oe_direct_line' + ); + if (links.length < 1) { + throw new Error("Category should have child page links"); + } + // Verify links have correct attributes + var link = links[0]; + if (!link.dataset.oeModel || !link.dataset.oeId) { + throw new Error("Category index link missing data-oe-model/id"); + } + if (link.getAttribute("target") === "_blank") { + throw new Error("Category index link should not open in new tab"); + } + }, + }, + ] +); diff --git a/document_page_reference/tests/__init__.py b/document_page_reference/tests/__init__.py index ca802a6bb24..05ff47b536c 100644 --- a/document_page_reference/tests/__init__.py +++ b/document_page_reference/tests/__init__.py @@ -1 +1,2 @@ from . import test_document_reference +from . import test_document_reference_tour diff --git a/document_page_reference/tests/test_document_reference.py b/document_page_reference/tests/test_document_reference.py index 4f16744490d..4089dc51987 100644 --- a/document_page_reference/tests/test_document_reference.py +++ b/document_page_reference/tests/test_document_reference.py @@ -75,4 +75,41 @@ def test_get_formview_action(self): def test_compute_content_parsed(self): self.page1.content = "

    " self.page1._compute_content_parsed() - self.assertEqual(str(self.page1.content_parsed), "

    ") + self.assertEqual(str(self.page1.content_parsed), "

    ") + + def test_content_parsed_category(self): + """A category page exposes its raw content as content_parsed.""" + category = self.page_obj.create( + { + "name": "Category", + "type": "category", + "content": "

    category body

    ", + "reference": "cat_ref", + } + ) + self.assertEqual(category.content_parsed, category.content) + + def test_get_page_index(self): + """_get_page_index renders an oe_direct_line anchor and nests + children in a list; a leaf page with link=False renders nothing.""" + parent = self.page_obj.create( + {"name": "Index Parent", "content": "x", "reference": "idx_parent"} + ) + self.page_obj.create( + { + "name": "Index Child", + "content": "y", + "reference": "idx_child", + "parent_id": parent.id, + } + ) + rendered = str(parent._get_page_index()) + self.assertIn('class="oe_direct_line"', rendered) + self.assertIn('data-oe-id="%s"' % parent.id, rendered) + self.assertIn("Index Parent", rendered) + self.assertIn("