From 6cb7238cf383aaf1fb0d7003b5b943e05f0c2dee Mon Sep 17 00:00:00 2001
From: Enric Tobella
Date: Wed, 17 Dec 2025 09:55:37 +0100
Subject: [PATCH 1/5] [FIX] document_page_approval: Fix tests to use real html
---
.../tests/test_document_page_approval.py | 26 +++++++++----------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/document_page_approval/tests/test_document_page_approval.py b/document_page_approval/tests/test_document_page_approval.py
index e9154ade084..f245788419d 100644
--- a/document_page_approval/tests/test_document_page_approval.py
+++ b/document_page_approval/tests/test_document_page_approval.py
@@ -38,7 +38,7 @@ def setUp(self):
{
"name": "This page requires approval",
"parent_id": self.category2.id,
- "content": "This content will require approval",
+ "content": "This content will require approval
",
}
)
@@ -73,22 +73,22 @@ def test_change_request_approve(self):
self.assertEqual(chreq.state, "approved")
self.assertEqual(chreq.content, page.content)
- # new changes should create change requests
- page.write({"content": "New content"})
- # Needed to compute calculated fields
- page.invalidate_model()
- self.assertNotEqual(page.content, "New content")
+ # Create new change request
+ page.write({"content": "New content
"})
+ page.invalidate_model() # Recompute fields
chreq = self.history_obj.search(
- [("page_id", "=", page.id), ("state", "!=", "approved")]
- )[0]
- chreq.action_approve()
- self.assertEqual(page.content, "New content")
+ [("page_id", "=", page.id), ("state", "!=", "approved")], limit=1
+ )
+
+ # Approve new changes
+ chreq.with_user(self.user2).action_approve()
+ self.assertEqual(page.content, "New content
")
def test_change_request_auto_approve(self):
page = self.page1
self.assertFalse(page.is_approval_required)
- page.write({"content": "New content"})
- self.assertEqual(page.content, "New content")
+ page.write({"content": "New content
"})
+ self.assertEqual(page.content, "New content
")
def test_change_request_from_scratch(self):
page = self.page2
@@ -103,7 +103,7 @@ def test_change_request_from_scratch(self):
{
"page_id": page.id,
"summary": "Changed something",
- "content": "New content",
+ "content": "New content
",
}
)
From 8cc1a353f7ecff5a687457d696048f1e20b595b6 Mon Sep 17 00:00:00 2001
From: Enric Tobella
Date: Wed, 17 Dec 2025 09:55:54 +0100
Subject: [PATCH 2/5] [FIX] document_page_reference: Fix tests to use HTML
---
document_page_reference/tests/test_document_reference.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/document_page_reference/tests/test_document_reference.py b/document_page_reference/tests/test_document_reference.py
index 1a2ba268021..4f16744490d 100644
--- a/document_page_reference/tests/test_document_reference.py
+++ b/document_page_reference/tests/test_document_reference.py
@@ -51,8 +51,8 @@ def test_auto_reference(self):
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",
+ "content": "this should have an empty reference "
+ "because reference must be unique
",
}
)
self.assertFalse(new_page_duplicated_name.reference)
From c08619e3ed3a11ed93e68233c4f94f51e4ae8cc0 Mon Sep 17 00:00:00 2001
From: Enric Tobella
Date: Mon, 15 Dec 2025 09:59:54 +0100
Subject: [PATCH 3/5] [IMP] document_page: Use Odoo HTML Diff comparison method
---
document_page/README.rst | 4 ++
document_page/__manifest__.py | 5 +-
document_page/models/document_page_history.py | 29 +++--------
document_page/readme/CONTRIBUTORS.rst | 4 ++
document_page/static/description/index.html | 4 ++
.../static/src/scss/document_page.scss | 52 ++++++++++++-------
.../src/scss/document_page_variables.scss | 5 ++
document_page/tests/test_document_page.py | 14 +++--
.../tests/test_document_page_history.py | 15 +++---
.../tests/test_document_page_show_diff.py | 11 ++--
document_page/views/document_page_history.xml | 1 +
.../wizard/document_page_show_diff.py | 2 +-
12 files changed, 88 insertions(+), 58 deletions(-)
create mode 100644 document_page/static/src/scss/document_page_variables.scss
diff --git a/document_page/README.rst b/document_page/README.rst
index f958b49472f..8ff1a6faa01 100644
--- a/document_page/README.rst
+++ b/document_page/README.rst
@@ -94,6 +94,10 @@ Trobz
* Ángel García de la Chica Herrera
+* `Dixmit `__:
+
+ * Enric Tobella
+
Other credits
~~~~~~~~~~~~~
diff --git a/document_page/__manifest__.py b/document_page/__manifest__.py
index 77c1698db6f..5d2e99ae00f 100644
--- a/document_page/__manifest__.py
+++ b/document_page/__manifest__.py
@@ -17,7 +17,7 @@
],
"website": "https://github.com/OCA/knowledge",
"license": "AGPL-3",
- "depends": ["mail", "document_knowledge"],
+ "depends": ["mail", "document_knowledge", "web_editor"],
"data": [
"security/document_page_security.xml",
"security/ir.model.access.csv",
@@ -30,6 +30,9 @@
],
"demo": ["demo/document_page.xml"],
"assets": {
+ "web._assets_primary_variables": [
+ "document_page/static/src/**/document_page_variables.scss",
+ ],
"web.assets_backend": [
"document_page/static/src/scss/document_page.scss",
],
diff --git a/document_page/models/document_page_history.py b/document_page/models/document_page_history.py
index 7b200e6abbb..3bfd369d64b 100644
--- a/document_page/models/document_page_history.py
+++ b/document_page/models/document_page_history.py
@@ -1,9 +1,12 @@
# Copyright (C) 2004-2010 Tiny SPRL ().
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-import difflib
-from odoo import _, api, fields, models
+from odoo import fields, models
+
+from odoo.addons.web_editor.models.diff_utils import (
+ generate_comparison,
+)
class DocumentPageHistory(models.Model):
@@ -17,7 +20,7 @@ class DocumentPageHistory(models.Model):
name = fields.Char(index=True)
summary = fields.Char(index=True)
content = fields.Html(sanitize=False)
- diff = fields.Html(compute="_compute_diff")
+ diff = fields.Html(compute="_compute_diff", sanitize_tags=False)
company_id = fields.Many2one(
"res.company",
@@ -43,28 +46,10 @@ def _compute_diff(self):
)
rec.diff = self._get_diff(prev.id, rec.id)
- @api.model
def _get_diff(self, v1, v2):
- """Return the difference between two version of document version."""
text1 = v1 and self.browse(v1).content or ""
text2 = v2 and self.browse(v2).content or ""
- # Include line breaks to make it more readable
- # TODO: consider using a beautify library directly on the content
- text1 = text1.replace("
", "
\r\n")
- text2 = text2.replace("
", "
\r\n")
- line1 = text1.splitlines(True)
- line2 = text2.splitlines(True)
- if line1 == line2:
- return _("There are no changes in revisions.")
- else:
- diff = difflib.HtmlDiff()
- return diff.make_table(
- line1,
- line2,
- f"Revision-{v1}",
- f"Revision-{v2}",
- context=True,
- )
+ return generate_comparison(text1, text2)
def name_get(self):
return [(rec.id, "%s #%i" % (rec.page_id.name, rec.id)) for rec in self]
diff --git a/document_page/readme/CONTRIBUTORS.rst b/document_page/readme/CONTRIBUTORS.rst
index a45e3a717c2..0aa38704d31 100644
--- a/document_page/readme/CONTRIBUTORS.rst
+++ b/document_page/readme/CONTRIBUTORS.rst
@@ -16,3 +16,7 @@ Trobz
* `Sygel `_:
* Ángel García de la Chica Herrera
+
+* `Dixmit `__:
+
+ * Enric Tobella
diff --git a/document_page/static/description/index.html b/document_page/static/description/index.html
index 634c1b38de0..8279f41202d 100644
--- a/document_page/static/description/index.html
+++ b/document_page/static/description/index.html
@@ -442,6 +442,10 @@
Ángel García de la Chica Herrera
+Dixmit:
+
diff --git a/document_page/static/src/scss/document_page.scss b/document_page/static/src/scss/document_page.scss
index 82cfbaaccc8..064d415a9dd 100644
--- a/document_page/static/src/scss/document_page.scss
+++ b/document_page/static/src/scss/document_page.scss
@@ -1,28 +1,40 @@
-table.diff {
- font-family: Courier;
- border: medium;
+.o_document_page_diff {
+ table.diff {
+ font-family: Courier;
+ border: medium;
- .diff_header {
- background-color: #e0e0e0;
- }
+ .diff_header {
+ background-color: $o_document_page_diff_header_background;
+ }
- td.diff_header {
- text-align: right;
- }
+ td.diff_header {
+ text-align: right;
+ }
- .diff_next {
- background-color: #c0c0c0;
- }
+ .diff_next {
+ background-color: $o_document_page_diff_next_background;
+ }
- .diff_add {
- background-color: #aaffaa;
- }
+ .diff_add {
+ background-color: $o_document_page_diff_add_background;
+ }
- .diff_chg {
- background-color: #ffff77;
- }
+ .diff_chg {
+ background-color: $o_document_page_diff_change_background;
+ }
- .diff_sub {
- background-color: #ffaaaa;
+ .diff_sub {
+ background-color: $o_document_page_diff_subtract_background;
+ }
+ }
+ removed {
+ display: inline;
+ text-decoration: line-through;
+ opacity: 0.5;
+ background-color: $o_document_page_diff_subtract_background;
+ }
+ added {
+ display: inline;
+ background-color: $o_document_page_diff_add_background;
}
}
diff --git a/document_page/static/src/scss/document_page_variables.scss b/document_page/static/src/scss/document_page_variables.scss
new file mode 100644
index 00000000000..0c4d02fedeb
--- /dev/null
+++ b/document_page/static/src/scss/document_page_variables.scss
@@ -0,0 +1,5 @@
+$o_document_page_diff_header_background: #e0e0e0;
+$o_document_page_diff_next_background: #c0c0c0;
+$o_document_page_diff_add_background: #aaffaa;
+$o_document_page_diff_change_background: #ffff77;
+$o_document_page_diff_subtract_background: #ffaaaa;
diff --git a/document_page/tests/test_document_page.py b/document_page/tests/test_document_page.py
index 22b03c2ae82..4ada5d71df4 100644
--- a/document_page/tests/test_document_page.py
+++ b/document_page/tests/test_document_page.py
@@ -31,12 +31,16 @@ def test_category_template(self):
self.assertEqual(page.content, self.category1.template)
def test_page_history_diff(self):
- page = self.page_obj.create({"name": "Test Page 3", "content": "Test content"})
- page.content = "New content"
+ page = self.page_obj.create(
+ {"name": "Test Page 3", "content": "
Test content
"}
+ )
+ page.content = "
New content
"
self.assertIsNotNone(page.history_ids[0].diff)
def test_page_link(self):
- page = self.page_obj.create({"name": "Test Page 3", "content": "Test content"})
+ page = self.page_obj.create(
+ {"name": "Test Page 3", "content": "
Test content
"}
+ )
self.assertEqual(
page.backend_url,
f"/web#id={page.id}&model=document.page&view_type=form",
@@ -51,7 +55,9 @@ def test_page_link(self):
)
def test_page_copy(self):
- page = self.page_obj.create({"name": "Test Page 3", "content": "Test content"})
+ page = self.page_obj.create(
+ {"name": "Test Page 3", "content": "
Test content
"}
+ )
page_copy = page.copy()
self.assertEqual(page_copy.name, page.name + " (copy)")
self.assertEqual(page_copy.content, page.content)
diff --git a/document_page/tests/test_document_page_history.py b/document_page/tests/test_document_page_history.py
index 856be77f783..dd09334191f 100644
--- a/document_page/tests/test_document_page_history.py
+++ b/document_page/tests/test_document_page_history.py
@@ -6,14 +6,15 @@ class TestDocumentPageHistory(common.TransactionCase):
def test_page_history_demo_page1(self):
"""Test page history demo page1."""
- page = self.env.ref("document_page.demo_page1")
- page.content = "Test content updated"
+ page = self.env["document.page"].create(
+ {
+ "name": "Test Page",
+ "content": "
Initial content
",
+ }
+ )
+ page.content = "
Test content updated
"
history_document = self.env["document.page.history"]
history_pages = history_document.search([("page_id", "=", page.id)])
active_ids = [i.id for i in history_pages]
-
result = history_document._get_diff(active_ids[0], active_ids[0])
- self.assertEqual(result, "There are no changes in revisions.")
-
- result = history_document._get_diff(active_ids[0], active_ids[1])
- self.assertNotEqual(result, "There are no changes in revisions.")
+ self.assertEqual(result, page.content)
diff --git a/document_page/tests/test_document_page_show_diff.py b/document_page/tests/test_document_page_show_diff.py
index 811c35e7cde..54f564ecc73 100644
--- a/document_page/tests/test_document_page_show_diff.py
+++ b/document_page/tests/test_document_page_show_diff.py
@@ -8,7 +8,12 @@ class TestDocumentPageShowDiff(common.TransactionCase):
def test_show_demo_page1_diff(self):
"""Show test page history difference."""
- page = self.env.ref("document_page.demo_page1")
+ page = self.env["document.page"].create(
+ {
+ "name": "Test Page",
+ "content": "
Initial content
",
+ }
+ )
show_diff_object = self.env["wizard.document.page.history.show_diff"]
@@ -21,8 +26,8 @@ def test_show_demo_page1_diff(self):
)._get_diff()
)
- page.write({"content": "Text content updated"})
- page.write({"content": "Text updated"})
+ page.write({"content": "
Text content updated
"})
+ page.write({"content": "
Text updated
"})
history_pages = history_document.search([("page_id", "=", page.id)])
diff --git a/document_page/views/document_page_history.xml b/document_page/views/document_page_history.xml
index 00107d75081..a01acfba7cf 100644
--- a/document_page/views/document_page_history.xml
+++ b/document_page/views/document_page_history.xml
@@ -70,6 +70,7 @@
diff --git a/document_page/wizard/document_page_show_diff.py b/document_page/wizard/document_page_show_diff.py
index 1557ff7b8d3..26f025421dc 100644
--- a/document_page/wizard/document_page_show_diff.py
+++ b/document_page/wizard/document_page_show_diff.py
@@ -27,4 +27,4 @@ def _get_diff(self):
raise UserError(_("Select one or maximum two history revisions!"))
return diff
- diff = fields.Html(readonly=True, default=_get_diff)
+ diff = fields.Html(readonly=True, default=_get_diff, sanitize_tags=False)
From 9fb2829fcda0acb375e4b8bfdcfd9ee09ed64b4f Mon Sep 17 00:00:00 2001
From: Enric Tobella
Date: Wed, 17 Dec 2025 18:59:18 +0100
Subject: [PATCH 4/5] [IMP] document_page: Include diff_utils from odoo
---
document_page/models/diff_utils.py | 242 ++++++++++++++++++
document_page/models/document_page_history.py | 2 +-
2 files changed, 243 insertions(+), 1 deletion(-)
create mode 100644 document_page/models/diff_utils.py
diff --git a/document_page/models/diff_utils.py b/document_page/models/diff_utils.py
new file mode 100644
index 00000000000..8cb7772a1e1
--- /dev/null
+++ b/document_page/models/diff_utils.py
@@ -0,0 +1,242 @@
+# This code has been ported from Odoo 18.0 web_editor module.
+# License of this part should remain LGPL-3.0 or later following Odoo's licensing.
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import re
+from difflib import SequenceMatcher
+
+# ------------------------------------------------------------
+# Patch and comparison functions
+# ------------------------------------------------------------
+
+
+OPERATION_SEPARATOR = "\n"
+LINE_SEPARATOR = "<"
+
+PATCH_OPERATION_LINE_AT = "@"
+PATCH_OPERATION_CONTENT = ":"
+
+PATCH_OPERATION_ADD = "+"
+PATCH_OPERATION_REMOVE = "-"
+PATCH_OPERATION_REPLACE = "R"
+
+PATCH_OPERATIONS = dict(
+ insert=PATCH_OPERATION_ADD,
+ delete=PATCH_OPERATION_REMOVE,
+ replace=PATCH_OPERATION_REPLACE,
+)
+
+HTML_ATTRIBUTES_TO_REMOVE = [
+ "data-last-history-steps",
+]
+
+
+HTML_TAG_ISOLATION_REGEX = r"^([^>]*>)(.*)$"
+ADDITION_COMPARISON_REGEX = r"\1\2"
+ADDITION_1ST_REPLACE_COMPARISON_REGEX = r"added>\2"
+DELETION_COMPARISON_REGEX = r"\1\2"
+EMPTY_OPERATION_TAG = r"<(added|removed)><\/(added|removed)>"
+SAME_TAG_REPLACE_FIXER = r"<\/added><(?:[^\/>]|(?:><))+>"
+UNNECESSARY_REPLACE_FIXER = (
+ r"([^<](?!<\/added>)*)<\/added>"
+ r"([^<](?!<\/removed>)*)<\/removed>"
+)
+
+
+def generate_comparison(new_content, old_content): # noqa: C901
+ """Compare a content to an older content
+ and generate a comparison html between both content.
+
+ :param string new_content: the current content
+ :param string old_content: the old content
+
+ :return: string: the comparison content
+ """
+ new_content = _remove_html_attribute(new_content, HTML_ATTRIBUTES_TO_REMOVE)
+ old_content = _remove_html_attribute(old_content, HTML_ATTRIBUTES_TO_REMOVE)
+
+ if new_content == old_content:
+ return new_content
+
+ patch = generate_patch(new_content, old_content)
+ comparison = new_content.split(LINE_SEPARATOR)
+ patch_operations = patch.split(OPERATION_SEPARATOR)
+ # We need to apply operation from last to the first
+ # to preserve the indexes integrity.
+ patch_operations.reverse()
+
+ for operation in patch_operations:
+ metadata, *patch_content_line = operation.split(LINE_SEPARATOR)
+
+ metadata_split = metadata.split(PATCH_OPERATION_LINE_AT)
+ operation_type = metadata_split[0]
+ lines_index_range = metadata_split[1] if len(metadata_split) > 1 else ""
+ lines_index_range = lines_index_range.split(PATCH_OPERATION_CONTENT)[0]
+ indexes = lines_index_range.split(",")
+ start_index = int(indexes[0])
+ end_index = int(indexes[1]) if len(indexes) > 1 else start_index
+
+ # If the operation is a replace, we need to flag the changes that
+ # will generate ghost opening tags if we don't ignore
+ # them.
+ # this can append when:
+ # * A change concerning only html parameters.
+ # a
=> a
+ # * An addition in a previously empty element opening tag
+ # => a
+ if operation_type == PATCH_OPERATION_REPLACE:
+ for i, line in enumerate(patch_content_line):
+ current_index = start_index + i
+ if current_index > end_index:
+ break
+
+ current_line = comparison[current_index]
+ current_line_tag = current_line.split(">")[0]
+ line_tag = line.split(">")[0]
+ if current_line[-1] == ">" and (
+ current_line_tag == line_tag
+ or current_line_tag.split(" ")[0] == line_tag.split(" ")[0]
+ ):
+ comparison[start_index + i] = "delete_me>"
+
+ # We need to insert lines from last to the first
+ # to preserve the indexes integrity.
+ patch_content_line.reverse()
+
+ for index in range(end_index, start_index - 1, -1):
+ if operation_type in [
+ PATCH_OPERATION_REMOVE,
+ PATCH_OPERATION_REPLACE,
+ ]:
+ deletion_flagged_comparison = re.sub(
+ HTML_TAG_ISOLATION_REGEX,
+ DELETION_COMPARISON_REGEX,
+ comparison[index],
+ )
+ # Only use this line if it doesn't generate an empty
+ # tag
+ if not re.search(EMPTY_OPERATION_TAG, deletion_flagged_comparison):
+ comparison[index] = deletion_flagged_comparison
+
+ if operation_type == PATCH_OPERATION_ADD:
+ for line in patch_content_line:
+ addition_flagged_line = re.sub(
+ HTML_TAG_ISOLATION_REGEX, ADDITION_COMPARISON_REGEX, line
+ )
+
+ if not re.search(EMPTY_OPERATION_TAG, addition_flagged_line):
+ comparison.insert(start_index + 1, addition_flagged_line)
+ else:
+ comparison.insert(start_index + 1, line)
+
+ if operation_type == PATCH_OPERATION_REPLACE:
+ for _i, line in enumerate(patch_content_line):
+ addition_flagged_line = re.sub(
+ HTML_TAG_ISOLATION_REGEX, ADDITION_COMPARISON_REGEX, line
+ )
+ if not re.search(EMPTY_OPERATION_TAG, addition_flagged_line):
+ comparison.insert(start_index, addition_flagged_line)
+ elif line.split(">")[0] != comparison[start_index].split(">")[0]:
+ comparison.insert(start_index, line)
+
+ final_comparison = LINE_SEPARATOR.join(comparison)
+ # We can remove all the opening tags which are located between the end of an
+ # added tag and the start of a removed tag, because this should never happen
+ # as the added and removed tags should always be near each other.
+ # This can happen when the new container tag had a parameter change.
+ final_comparison = re.sub(
+ SAME_TAG_REPLACE_FIXER, "", final_comparison
+ )
+
+ # Remove al the tags
+ final_comparison = final_comparison.replace(r"", "")
+
+ # This fix the issue of unnecessary replace tags.
+ # ex: abcabc -> abc
+ # This can occur when the new content is the same as the old content and
+ # their container tags are the same but the tags parameters are different
+ for match in re.finditer(UNNECESSARY_REPLACE_FIXER, final_comparison):
+ if match.group(1) == match.group(2):
+ final_comparison = final_comparison.replace(match.group(0), match.group(1))
+
+ return final_comparison
+
+
+def _format_line_index(start, end):
+ """Format the line index to be used in a patch operation.
+
+ :param start: the start index
+ :param end: the end index
+ :return: string
+ """
+ length = end - start
+ if not length:
+ start -= 1
+ if length <= 1:
+ return f"{PATCH_OPERATION_LINE_AT}{start}"
+ return f"{PATCH_OPERATION_LINE_AT}{start},{start + length - 1}"
+
+
+def _patch_generator(new_content, old_content):
+ """Generate a patch (multiple operations) between two contents.
+ Each operation is a string with the following format:
+ @[,][:*]
+ patch format example:
+ +@4:ab
cd
+ +@4,15:ef
gh
+ -@32
+ -@125,129
+ R@523:sdf
+
+ :param string new_content: the new content
+ :param string old_content: the old content
+
+ :return: string: the patch containing all the operations to reverse
+ the new content to the old content
+ """
+ # remove break line in contents to ensure they don't interfere with
+ # operations
+ new_content = new_content.replace("\n", "")
+ old_content = old_content.replace("\n", "")
+
+ new_content_lines = new_content.split(LINE_SEPARATOR)
+ old_content_lines = old_content.split(LINE_SEPARATOR)
+
+ for group in SequenceMatcher(
+ None, new_content_lines, old_content_lines, False
+ ).get_grouped_opcodes(0):
+ patch_content_line = []
+ first, last = group[0], group[-1]
+ patch_operation = _format_line_index(first[1], last[2])
+
+ if any(tag in {"replace", "delete"} for tag, _, _, _, _ in group):
+ for tag, _, _, _, _ in group:
+ if tag not in {"insert", "equal", "replace"}:
+ patch_operation = PATCH_OPERATIONS[tag] + patch_operation
+
+ if any(tag in {"replace", "insert"} for tag, _, _, _, _ in group):
+ for tag, _, _, j1, j2 in group:
+ if tag not in {"delete", "equal"}:
+ patch_operation = PATCH_OPERATIONS[tag] + patch_operation
+ for line in old_content_lines[j1:j2]:
+ patch_content_line.append(line)
+
+ if patch_content_line:
+ patch_content = LINE_SEPARATOR + LINE_SEPARATOR.join(patch_content_line)
+ yield str(patch_operation) + PATCH_OPERATION_CONTENT + patch_content
+ else:
+ yield str(patch_operation)
+
+
+def generate_patch(new_content, old_content):
+ new_content = _remove_html_attribute(new_content, HTML_ATTRIBUTES_TO_REMOVE)
+ old_content = _remove_html_attribute(old_content, HTML_ATTRIBUTES_TO_REMOVE)
+
+ return OPERATION_SEPARATOR.join(list(_patch_generator(new_content, old_content)))
+
+
+def _remove_html_attribute(html_content, attributes_to_remove):
+ for attribute in attributes_to_remove:
+ html_content = re.sub(rf' {attribute}="[^"]*"', "", html_content)
+
+ return html_content
diff --git a/document_page/models/document_page_history.py b/document_page/models/document_page_history.py
index 3bfd369d64b..45d8b077a2a 100644
--- a/document_page/models/document_page_history.py
+++ b/document_page/models/document_page_history.py
@@ -4,7 +4,7 @@
from odoo import fields, models
-from odoo.addons.web_editor.models.diff_utils import (
+from .diff_utils import (
generate_comparison,
)
From a3fcb764be33f5c9bb5cf10947187d12736458bb Mon Sep 17 00:00:00 2001
From: Enric Tobella
Date: Fri, 19 Dec 2025 13:05:33 +0100
Subject: [PATCH 5/5] [FIX] document_page_approval: Add email on test user
---
document_page_approval/tests/test_document_page_approval.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/document_page_approval/tests/test_document_page_approval.py b/document_page_approval/tests/test_document_page_approval.py
index f245788419d..30b51609601 100644
--- a/document_page_approval/tests/test_document_page_approval.py
+++ b/document_page_approval/tests/test_document_page_approval.py
@@ -25,6 +25,7 @@ def setUp(self):
],
}
)
+ self.user2.partner_id.write({"email": "test@test.com"})
# demo_approval
self.category2 = self.page_obj.create(
{