Skip to content

Commit 52287c1

Browse files
committed
[FIX] document_page_reference: migrate widget to OWL for Odoo 16
1 parent 8f372fe commit 52287c1

9 files changed

Lines changed: 277 additions & 53 deletions

File tree

document_page_reference/__manifest__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
"web.assets_backend": [
1919
"document_page_reference/static/src/js/**/*",
2020
],
21+
"web.assets_tests": [
22+
"document_page_reference/static/tests/**/*",
23+
],
2124
},
2225
"maintainers": ["etobella"],
2326
}

document_page_reference/models/document_page.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import logging
55

6+
from markupsafe import Markup
7+
68
from odoo import _, api, fields, models, tools
79
from odoo.exceptions import ValidationError
810
from odoo.tools.misc import html_escape
@@ -52,25 +54,53 @@ class DocumentPage(models.Model):
5254
reference = fields.Char(
5355
help="Used to find the document, it can contain letters, numbers and _"
5456
)
55-
content_parsed = fields.Html(compute="_compute_content_parsed")
57+
content_parsed = fields.Html(compute="_compute_content_parsed", sanitize=False)
58+
59+
def _get_page_index(self, link=True):
60+
"""Override to use oe_direct_line links compatible with the widget."""
61+
self.ensure_one()
62+
index = [
63+
Markup("<li>") + subpage._get_page_index() + Markup("</li>")
64+
for subpage in self.child_ids
65+
]
66+
r = Markup("")
67+
if link:
68+
r = (
69+
Markup(
70+
'<a href="#" class="oe_direct_line"'
71+
' data-oe-model="%s" data-oe-id="%s">'
72+
)
73+
% (
74+
self._name,
75+
self.id,
76+
)
77+
+ html_escape(self.name)
78+
+ Markup("</a>")
79+
)
80+
if index:
81+
r += Markup("<ul>") + Markup("").join(index) + Markup("</ul>")
82+
return r
5683

5784
def get_formview_action(self, access_uid=None):
5885
res = super().get_formview_action(access_uid)
5986
view_id = self.env.ref("document_page.view_wiki_form").id
6087
res["views"] = [(view_id, "form")]
6188
return res
6289

63-
@api.depends("history_head")
90+
@api.depends("history_head", "type")
6491
def _compute_content_parsed(self):
6592
for record in self:
66-
content = record.get_content()
67-
if content == "<p>" and self.content != "<p>":
68-
_logger.error(
69-
"Template from page with id = %s cannot be processed correctly"
70-
% self.id
71-
)
72-
content = self.content
73-
record.content_parsed = content
93+
if record.type == "category":
94+
record.content_parsed = record.content
95+
else:
96+
content = record.get_content()
97+
if content == "<p>" and record.content != "<p>":
98+
_logger.error(
99+
"Template from page with id = %s cannot be "
100+
"processed correctly" % record.id
101+
)
102+
content = record.content
103+
record.content_parsed = content
74104

75105
@api.constrains("reference")
76106
def _check_reference(self):
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* @odoo-module */
2+
3+
import {HtmlField} from "@web_editor/js/backend/html_field";
4+
import {registry} from "@web/core/registry";
5+
import {useService} from "@web/core/utils/hooks";
6+
import {onMounted, onPatched} from "@odoo/owl";
7+
8+
export class DocumentPageReferenceField extends HtmlField {
9+
setup() {
10+
super.setup();
11+
this.actionService = useService("action");
12+
this.orm = useService("orm");
13+
this._onClickDirectLink = this._onClickDirectLink.bind(this);
14+
onMounted(() => this._bindLinks());
15+
onPatched(() => this._bindLinks());
16+
}
17+
_bindLinks() {
18+
const el = this.readonlyElementRef && this.readonlyElementRef.el;
19+
if (!el) return;
20+
// Remove target="_blank" from internal reference links
21+
// (added by retargetLinks in HtmlField)
22+
for (const link of el.querySelectorAll("a.oe_direct_line")) {
23+
link.removeAttribute("target");
24+
link.removeAttribute("rel");
25+
link.removeEventListener("click", this._onClickDirectLink);
26+
link.addEventListener("click", this._onClickDirectLink);
27+
}
28+
}
29+
_onClickDirectLink(ev) {
30+
ev.preventDefault();
31+
ev.stopPropagation();
32+
const target = ev.currentTarget;
33+
const model = target.dataset.oeModel;
34+
const id = parseInt(target.dataset.oeId, 10);
35+
if (!model || !id) return;
36+
this.orm.call(model, "get_formview_action", [[id]]).then((action) => {
37+
this.actionService.doAction(action);
38+
});
39+
}
40+
}
41+
42+
registry.category("fields").add("document_page_reference", DocumentPageReferenceField);

document_page_reference/static/src/js/editor.js

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/** @odoo-module */
2+
3+
import tour from "web_tour.tour";
4+
5+
/*
6+
* Test 1: Reference widget renders ${ref} as clickable links.
7+
*/
8+
tour.register(
9+
"document_page_reference_widget_tour",
10+
{
11+
test: true,
12+
url: "/web#action=document_page.action_page",
13+
},
14+
[
15+
{
16+
content: "Open Test Ref Page 1",
17+
trigger: '.o_data_cell[name="name"]:contains("Test Ref Page 1")',
18+
run: "click",
19+
},
20+
{
21+
content: "Verify content_parsed renders reference as link",
22+
trigger:
23+
'.o_form_view .o_field_widget[name="content_parsed"] a.oe_direct_line',
24+
timeout: 20000,
25+
run: function () {
26+
var link = this.$anchor[0];
27+
if (!link.dataset.oeModel || !link.dataset.oeId) {
28+
throw new Error("Reference link missing data-oe-model/data-oe-id");
29+
}
30+
if (link.getAttribute("target") === "_blank") {
31+
throw new Error(
32+
"Internal reference link should not have target=_blank"
33+
);
34+
}
35+
},
36+
},
37+
]
38+
);
39+
40+
/*
41+
* Test 2: Category page index uses oe_direct_line links.
42+
*/
43+
tour.register(
44+
"document_page_reference_category_tour",
45+
{
46+
test: true,
47+
url: "/web#action=document_page.action_page",
48+
},
49+
[
50+
{
51+
content: "Open Test Ref Page 1 to navigate to its category",
52+
trigger: '.o_data_cell[name="name"]:contains("Test Ref Page 1")',
53+
run: "click",
54+
},
55+
{
56+
content: "Navigate to category via parent_id link",
57+
trigger: '.o_form_view .o_field_widget[name="parent_id"] a',
58+
timeout: 20000,
59+
run: "click",
60+
},
61+
{
62+
content: "Verify category shows child links as oe_direct_line",
63+
trigger:
64+
'.o_form_view .o_field_widget[name="content_parsed"] a.oe_direct_line',
65+
timeout: 20000,
66+
run: function () {
67+
var links = document.querySelectorAll(
68+
'.o_field_widget[name="content_parsed"] a.oe_direct_line'
69+
);
70+
if (links.length < 1) {
71+
throw new Error("Category should have child page links");
72+
}
73+
// Verify links have correct attributes
74+
var link = links[0];
75+
if (!link.dataset.oeModel || !link.dataset.oeId) {
76+
throw new Error("Category index link missing data-oe-model/id");
77+
}
78+
if (link.getAttribute("target") === "_blank") {
79+
throw new Error("Category index link should not open in new tab");
80+
}
81+
},
82+
},
83+
]
84+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from . import test_document_reference
2+
from . import test_document_reference_tour

document_page_reference/tests/test_document_reference.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,41 @@ def test_get_formview_action(self):
7575
def test_compute_content_parsed(self):
7676
self.page1.content = "<p>"
7777
self.page1._compute_content_parsed()
78-
self.assertEqual(str(self.page1.content_parsed), "<p></p>")
78+
self.assertEqual(str(self.page1.content_parsed), "<p>")
79+
80+
def test_content_parsed_category(self):
81+
"""A category page exposes its raw content as content_parsed."""
82+
category = self.page_obj.create(
83+
{
84+
"name": "Category",
85+
"type": "category",
86+
"content": "<p>category body</p>",
87+
"reference": "cat_ref",
88+
}
89+
)
90+
self.assertEqual(category.content_parsed, category.content)
91+
92+
def test_get_page_index(self):
93+
"""_get_page_index renders an oe_direct_line anchor and nests
94+
children in a list; a leaf page with link=False renders nothing."""
95+
parent = self.page_obj.create(
96+
{"name": "Index Parent", "content": "x", "reference": "idx_parent"}
97+
)
98+
self.page_obj.create(
99+
{
100+
"name": "Index Child",
101+
"content": "y",
102+
"reference": "idx_child",
103+
"parent_id": parent.id,
104+
}
105+
)
106+
rendered = str(parent._get_page_index())
107+
self.assertIn('class="oe_direct_line"', rendered)
108+
self.assertIn('data-oe-id="%s"' % parent.id, rendered)
109+
self.assertIn("Index Parent", rendered)
110+
self.assertIn("<ul>", rendered)
111+
self.assertIn("Index Child", rendered)
112+
leaf = self.page_obj.create(
113+
{"name": "Leaf", "content": "z", "reference": "leaf_ref"}
114+
)
115+
self.assertEqual(str(leaf._get_page_index(link=False)), "")
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright 2025 TRIVAX INNOVA SL
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
4+
from odoo.tests import HttpCase, tagged
5+
6+
7+
@tagged("post_install", "-at_install")
8+
class TestDocumentPageReferenceTour(HttpCase):
9+
@classmethod
10+
def setUpClass(cls):
11+
super().setUpClass()
12+
# Ensure admin password is known for the tour
13+
cls.env["res.users"].browse(2).write({"password": "admin"})
14+
page_obj = cls.env["document.page"]
15+
# Create a category
16+
cls.category = page_obj.create(
17+
{
18+
"name": "Test Ref Category",
19+
"type": "category",
20+
}
21+
)
22+
# Create two pages with cross-references
23+
cls.page1 = page_obj.create(
24+
{
25+
"name": "Test Ref Page 1",
26+
"parent_id": cls.category.id,
27+
"content": "<p>Go to ${r2} for details.</p>",
28+
"reference": "r1",
29+
}
30+
)
31+
cls.page2 = page_obj.create(
32+
{
33+
"name": "Test Ref Page 2",
34+
"parent_id": cls.category.id,
35+
"content": "<p>Back to ${r1} for overview.</p>",
36+
"reference": "r2",
37+
}
38+
)
39+
40+
def test_reference_widget_renders_links(self):
41+
"""Test that the document_page_reference widget renders ${ref}
42+
as clickable links and navigates without opening new tabs."""
43+
self.start_tour(
44+
"/web#action=document_page.action_page",
45+
"document_page_reference_widget_tour",
46+
login="admin",
47+
timeout=60,
48+
)
49+
50+
def test_category_index_links(self):
51+
"""Test that category pages show child links as oe_direct_line
52+
without duplicating content."""
53+
self.start_tour(
54+
"/web#action=document_page.action_page",
55+
"document_page_reference_category_tour",
56+
login="admin",
57+
timeout=60,
58+
)

document_page_reference/views/document_page.xml

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,36 @@
1515
/>
1616
</h2>
1717
</xpath>
18-
<field name="content" position="attributes">
19-
<attribute name="class">oe_edit_only</attribute>
20-
</field>
2118
<field name="content" position="before">
2219
<field
2320
name="content_parsed"
24-
class="oe_read_only"
21+
readonly="1"
2522
widget="document_page_reference"
2623
/>
2724
</field>
25+
<field name="content" position="attributes">
26+
<attribute name="attrs">
27+
{'invisible': [('type', '=', 'category')],
28+
'required': [('type', '!=', 'category')]}
29+
</attribute>
30+
</field>
2831
</field>
2932
</record>
3033
<record id="view_wiki_menu_form" model="ir.ui.view">
3134
<field name="name">document.page.menu.form</field>
3235
<field name="model">document.page</field>
3336
<field name="inherit_id" ref="document_page.view_wiki_menu_form" />
3437
<field name="arch" type="xml">
35-
<field name="content" position="attributes">
36-
<attribute name="class">oe_edit_only</attribute>
37-
</field>
3838
<field name="content" position="before">
3939
<field
4040
name="content_parsed"
41-
class="oe_read_only"
41+
readonly="1"
4242
widget="document_page_reference"
4343
/>
4444
</field>
45+
<field name="content" position="attributes">
46+
<attribute name="invisible">1</attribute>
47+
</field>
4548
</field>
4649
</record>
4750
<record model="ir.ui.view" id="document_page_search_view">

0 commit comments

Comments
 (0)