From 4ba3272d019c8dea3ac0c706f1886ff066b9fcd0 Mon Sep 17 00:00:00 2001 From: Anusha Date: Fri, 13 Jun 2025 18:13:44 +0200 Subject: [PATCH] [MIG] document_page_reference: Migration to 17.0 [FIX]document_page_reference :log warning updated [FIX]document_page_reference: null value issue fixed Co-authored-by: Anusha --- document_page_reference/README.rst | 2 +- document_page_reference/__manifest__.py | 9 +- .../migrations/17.0.1.0.0/post-migration.py | 12 ++ .../models/document_page.py | 155 +++++------------- document_page_reference/readme/USAGE.md | 2 +- .../static/description/index.html | 2 +- .../static/src/js/editor.esm.js | 33 ++++ .../static/src/js/editor.js | 34 ---- .../tests/test_document_reference.py | 59 ++++--- .../views/report_document_page.xml | 6 +- 10 files changed, 128 insertions(+), 186 deletions(-) create mode 100644 document_page_reference/migrations/17.0.1.0.0/post-migration.py create mode 100644 document_page_reference/static/src/js/editor.esm.js delete mode 100644 document_page_reference/static/src/js/editor.js diff --git a/document_page_reference/README.rst b/document_page_reference/README.rst index 2ec66faa..3393888b 100644 --- a/document_page_reference/README.rst +++ b/document_page_reference/README.rst @@ -39,7 +39,7 @@ the link between document pages. Usage ===== -When editing a document page add elements like ${XXX} where XXX is the +When editing a document page add elements like {{XXX}} where XXX is the reference of another page. Now, when viewing the document, it will link directly to the page. Also, the name will be parsed as the display name. diff --git a/document_page_reference/__manifest__.py b/document_page_reference/__manifest__.py index 2c6b56fd..871049c6 100644 --- a/document_page_reference/__manifest__.py +++ b/document_page_reference/__manifest__.py @@ -3,20 +3,19 @@ { "name": "Document Page Reference", - "summary": """ - Include references on document pages""", - "version": "16.0.1.0.1", + "summary": "Include references on document pages", + "version": "17.0.1.0.0", "license": "AGPL-3", "author": "Creu Blanca,Odoo Community Association (OCA)", "website": "https://github.com/OCA/knowledge", - "depends": ["document_page", "web_editor"], + "depends": ["document_page"], "data": [ "views/document_page.xml", "views/report_document_page.xml", ], "assets": { "web.assets_backend": [ - "document_page_reference/static/src/js/**/*", + "document_page_reference/static/src/js/editor.esm.js", ], }, "maintainers": ["etobella"], diff --git a/document_page_reference/migrations/17.0.1.0.0/post-migration.py b/document_page_reference/migrations/17.0.1.0.0/post-migration.py new file mode 100644 index 00000000..0f15f1eb --- /dev/null +++ b/document_page_reference/migrations/17.0.1.0.0/post-migration.py @@ -0,0 +1,12 @@ +# Copyright 2025 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import re + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + for item in env["document.page"].search([("content", "ilike", "${")]): + item.content = re.sub(r"\${(.+)}", r"{{\1}}", item.content) diff --git a/document_page_reference/models/document_page.py b/document_page_reference/models/document_page.py index 5c5fd37e..0fa4b68b 100644 --- a/document_page_reference/models/document_page.py +++ b/document_page_reference/models/document_page.py @@ -1,50 +1,17 @@ # Copyright 2019 Creu Blanca +# Copyright 2025 Tecnativa - Víctor Martínez # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import logging +import re -from odoo import _, api, fields, models, tools +from markupsafe import Markup + +from odoo import _, api, fields, models from odoo.exceptions import ValidationError -from odoo.tools.misc import html_escape +from odoo.tools import html_escape from odoo.addons.http_routing.models.ir_http import slugify -_logger = logging.getLogger(__name__) - -try: - import re - - from jinja2 import Undefined - from jinja2.lexer import name_re as old_name_re - from jinja2.sandbox import SandboxedEnvironment - - name_re = re.compile("^%s$" % old_name_re.pattern) - - class Context(SandboxedEnvironment.context_class): - def resolve(self, key): - res = super().resolve(key) - if not isinstance(res, Undefined): - return res - return self.parent["ref"](key) - - class Environment(SandboxedEnvironment): - context_class = Context - - mako_template_env = Environment( - block_start_string="<%", - block_end_string="%>", - variable_start_string="${", - variable_end_string="}", - comment_start_string="<%doc>", - comment_end_string="", - line_statement_prefix="%", - line_comment_prefix="##", - trim_blocks=True, # do not output newline after blocks - autoescape=False, - ) -except Exception: - _logger.error("Jinja2 is not available") - class DocumentPage(models.Model): _inherit = "document.page" @@ -52,7 +19,9 @@ 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") + content_parsed = fields.Html( + "Parsed Content", compute="_compute_content_parsed", sanitize=False, store=True + ) def get_formview_action(self, access_uid=None): res = super().get_formview_action(access_uid) @@ -60,90 +29,56 @@ class DocumentPage(models.Model): res["views"] = [(view_id, "form")] return res - @api.depends("history_head") + @api.depends("content") 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 + record.content_parsed = record.get_content() @api.constrains("reference") - def _check_reference(self): - for record in self: - if not record.reference: + def _check_reference_validity(self): + for rec in self: + if not rec.reference: continue - record._validate_reference(record=record) - - @api.model - def _validate_reference(self, record=None, reference=None): - if not reference: - reference = self.reference - if not name_re.match(reference): - raise ValidationError(_("Reference is not valid")) - uniq_domain = [("reference", "=", reference)] - if record: - uniq_domain += [("id", "!=", record.id)] - if self.search(uniq_domain): - raise ValidationError(_("Reference must be unique")) + regex = r"^[a-zA-Z_][a-zA-Z0-9_]*$" + if not re.match(regex, rec.reference): + raise ValidationError(_("Reference is not valid")) + domain = [("reference", "=", rec.reference), ("id", "!=", rec.id)] + if self.search(domain): + raise ValidationError(_("Reference must be unique")) def _get_document(self, code): - # Hook created in order to add check on other models - document = self.search([("reference", "=", code)]) - if document: - return document - else: - return self.env[self._name] - - def get_reference(self, code): - element = self._get_document(code) - if self.env.context.get("raw_reference", False): - return html_escape(element.display_name) - text = """%s - """ - if not element: - text = "%s" % text - res = text % ( - element._name, - element and element.id or "", - code, - html_escape(element.display_name or code), - ) - return res - - def _get_template_variables(self): - return {"ref": self.get_reference} + return self.search([("reference", "=", code)], limit=1) def get_content(self): - try: - content = self.content - mako_env = mako_template_env - template = mako_env.from_string(tools.ustr(content)) - return template.render(self._get_template_variables()) - except Exception: - _logger.error( - "Template from page with id = %s cannot be processed" % self.id + self.ensure_one() + content_parsed = raw = self.content or "" + for text in re.findall(r"\{\{.*?\}\}", raw): + reference = text.replace("{{", "").replace("}}", "") + content_parsed = content_parsed.replace( + text, self._resolve_reference(reference) ) - return self.content + return content_parsed + + def _resolve_reference(self, code): + doc = self._get_document(code) + if self.env.context.get("raw_reference", False): + return html_escape(doc.display_name if doc else code) + sanitized_code = html_escape(code) + oe_model = doc._name if doc else self._name + oe_id = doc.id if doc else "" + name = html_escape(doc.display_name) if doc else sanitized_code + return ( + f"" + f"{name}" + ) def get_raw_content(self): - return self.with_context(raw_reference=True).get_content() + return Markup(self.with_context(raw_reference=True).get_content()) @api.model_create_multi def create(self, vals_list): for vals in vals_list: - if not vals.get("reference"): - # Propose a default reference - reference = slugify(vals.get("name")).replace("-", "_") - try: - self._validate_reference(reference=reference) - vals["reference"] = reference - except ValidationError: # pylint: disable=W8138 - # Do not fill reference. - pass + if not vals.get("reference") and vals.get("name"): + vals["reference"] = slugify(vals["name"]).replace("-", "_") return super().create(vals_list) diff --git a/document_page_reference/readme/USAGE.md b/document_page_reference/readme/USAGE.md index 6fa5061f..6a9530ff 100644 --- a/document_page_reference/readme/USAGE.md +++ b/document_page_reference/readme/USAGE.md @@ -1,3 +1,3 @@ -When editing a document page add elements like \${XXX} where XXX is the +When editing a document page add elements like {{XXX}} where XXX is the reference of another page. Now, when viewing the document, it will link directly to the page. Also, the name will be parsed as the display name. diff --git a/document_page_reference/static/description/index.html b/document_page_reference/static/description/index.html index 4942bf8f..19d0a2ff 100644 --- a/document_page_reference/static/description/index.html +++ b/document_page_reference/static/description/index.html @@ -387,7 +387,7 @@ the link between document pages.

Usage

-

When editing a document page add elements like ${XXX} where XXX is the +

When editing a document page add elements like {{XXX}} where XXX is the reference of another page. Now, when viewing the document, it will link directly to the page. Also, the name will be parsed as the display name.

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 00000000..302fd2ef --- /dev/null +++ b/document_page_reference/static/src/js/editor.esm.js @@ -0,0 +1,33 @@ +/** @odoo-module **/ +import {HtmlField, htmlField} from "@web/views/fields/html/html_field"; +import {onMounted} from "@odoo/owl"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; + +class DocumentPageReferenceField extends HtmlField { + setup() { + super.setup(); + this.orm = useService("orm"); + this.action = useService("action"); + onMounted(() => { + // eslint-disable-next-line no-undef + const links = document.querySelectorAll(".oe_direct_line"); + links.forEach((link) => { + link.addEventListener("click", (event) => + this._onClickDirectLink(event) + ); + }); + }); + } + _onClickDirectLink(event) { + const {oeModel: model, oeId} = event.target.dataset; + const id = parseInt(oeId, 10); + this.orm.call(model, "get_formview_action", [[id]], {}).then((action) => { + this.action.doAction(action); + }); + } +} +registry.category("fields").add("document_page_reference", { + ...htmlField, + component: 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 852df084..00000000 --- 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/tests/test_document_reference.py b/document_page_reference/tests/test_document_reference.py index 1a2ba268..7ef9b644 100644 --- a/document_page_reference/tests/test_document_reference.py +++ b/document_page_reference/tests/test_document_reference.py @@ -1,78 +1,75 @@ # Copyright 2019 Creu Blanca +# Copyright 2025 Tecnativa - Víctor Martínez # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from markupsafe import Markup from odoo.exceptions import ValidationError -from odoo.tests.common import TransactionCase + +from odoo.addons.base.tests.common import BaseCommon -class TestDocumentReference(TransactionCase): +class TestDocumentReference(BaseCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.page_obj = cls.env["document.page"] cls.history_obj = cls.env["document.page.history"] cls.page1 = cls.page_obj.create( - {"name": "Test Page 1", "content": "${r2}", "reference": "R1"} + {"name": "Test Page 1", "content": Markup("{{r2}}"), "reference": "R1"} ) cls.page2 = cls.page_obj.create( - {"name": "Test Page 1", "content": "${r1}", "reference": "r2"} + {"name": "Test Page 1", "content": Markup("{{r1}}"), "reference": "r2"} ) - def test_constrains_01(self): + def test_constraints_duplicate_reference(self): + """Should raise if reference is not unique (same as another).""" with self.assertRaises(ValidationError): self.page2.write({"reference": self.page1.reference}) - def test_constrains_02(self): + def test_constraints_invalid_reference(self): + """Should raise if reference does not match the required pattern.""" with self.assertRaises(ValidationError): self.page2.write({"reference": self.page2.reference + "-02"}) def test_no_contrains(self): self.page1.write({"reference": False}) + self.assertFalse(self.page1.reference) self.page2.write({"reference": False}) - self.assertEqual(self.page1.reference, self.page2.reference) + self.assertFalse(self.page2.reference) def test_check_raw(self): self.assertEqual(self.page2.display_name, self.page1.get_raw_content()) - def test_check_reference(self): - self.assertRegex(self.page1.content_parsed, ".*%s.*" % self.page2.display_name) - - def test_no_reference(self): - self.page2.reference = "r3" - self.assertRegex(self.page1.content_parsed, ".*r2.*") - def test_auto_reference(self): """Test if reference is proposed when saving a page without one.""" self.assertEqual(self.page1.reference, "R1") new_page = self.page_obj.create( - {"name": "Test Page with no rEfErenCe", "content": "some content"} + {"name": "Test Page with no reference", "content": "some content"} ) self.assertEqual(new_page.reference, "test_page_with_no_reference") - new_page_duplicated_name = self.page_obj.create( - { - "name": "test page with no reference", - "content": "this should have an empty reference " - "because reference must be unique", - } - ) - self.assertFalse(new_page_duplicated_name.reference) + with self.assertRaises(ValidationError): + new_page_duplicated_name = self.page_obj.create( + { + "name": "test page with no reference", + "content": "this should have an empty reference " + "because reference must be unique", + } + ) + self.assertFalse(new_page_duplicated_name.reference) def test_get_formview_action(self): res = self.page1.get_formview_action() view_id = self.env.ref("document_page.view_wiki_form").id - expected_result = { + expected_keys = { "type": "ir.actions.act_window", - "context": {}, "res_model": "document.page", "res_id": self.page1.id, - "view_mode": "form", - "view_type": "form", "target": "current", "views": [(view_id, "form")], } - self.assertEqual(res, expected_result) + for key, expected_value in expected_keys.items(): + self.assertEqual(res.get(key), expected_value, f"Mismatch in key: {key}") def test_compute_content_parsed(self): - self.page1.content = "

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

") + self.page1.content = Markup("

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

")) diff --git a/document_page_reference/views/report_document_page.xml b/document_page_reference/views/report_document_page.xml index 5ecaa150..de2e4379 100644 --- a/document_page_reference/views/report_document_page.xml +++ b/document_page_reference/views/report_document_page.xml @@ -4,11 +4,11 @@ id="report_documentpage_doc" inherit_id="document_page.report_documentpage_doc" > - + 1==0 - -
+ +