From 0a650040c1d49d203507d079e25c1e4b429745ae Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Wed, 6 May 2026 17:28:55 +0200 Subject: [PATCH 01/10] [IMP] base_view_inheritance_extension: better error messages --- .../models/ir_ui_view.py | 19 +++++++++++++++++++ .../test_base_view_inheritance_extension.py | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/base_view_inheritance_extension/models/ir_ui_view.py b/base_view_inheritance_extension/models/ir_ui_view.py index fa16930cd49..6207d642014 100644 --- a/base_view_inheritance_extension/models/ir_ui_view.py +++ b/base_view_inheritance_extension/models/ir_ui_view.py @@ -187,6 +187,7 @@ def _inheritance_handler_attributes_update(self, source, specs): """ node = self.locate_node(source, specs) + self._assert_node(node, specs) for spec in specs: attr_name = spec.get("name") # Parse ast from both node and spec @@ -212,6 +213,7 @@ def _inheritance_handler_attributes_text_add(self, source, specs): """ node = self.locate_node(source, specs) + self._assert_node(node, specs) for attribute_node in specs: attribute_name = attribute_node.get("name") old_value = node.get(attribute_name) or "" @@ -230,6 +232,7 @@ def _inheritance_handler_attributes_domain_add(self, source, specs): """ node = self.locate_node(source, specs) + self._assert_node(node, specs) for attribute_node in specs: attribute_name = attribute_node.get("name") condition = attribute_node.get("condition") @@ -275,3 +278,19 @@ def _str2var_domain_text(self, domain_str): domain_str = pattern.sub(r"\1", domain_str) pattern = re.compile(r"'([^']+)_is_a_var_to_replace'") return pattern.sub(r"\1", domain_str) + + @api.model + def _assert_node(self, node, specs): + """Raise a ValueError detailing the failing specification if node is None""" + if node is None: + self._raise_view_error( + self.env._( + "Element '%s' cannot be located in parent view", + etree.tostring( + etree.Element( + specs.tag, + {a: v for a, v in specs.attrib.items() if a != "position"}, + ) + ).decode("utf8"), + ) + ) diff --git a/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py b/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py index 5052859e97e..79e1d93acff 100644 --- a/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py +++ b/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py @@ -267,3 +267,11 @@ def test_wraptext(self): inherited_view.write( {"arch": ''} ) + + def test_locate_failure_error_message(self): + """Be sure there is a helpful error message when locating nodes fails""" + source = etree.fromstring("
") + specs = etree.fromstring('') + with self.assertRaisesRegex(ValueError, "nonexistent.*parent view") as caught: + self.env["ir.ui.view"].apply_inheritance_specs(source, specs) + self.assertTrue(caught.exception.context) From 7abe0e36c7c757256c0817cf94466b9b58b3f516 Mon Sep 17 00:00:00 2001 From: Moises Lopez - Vauxoo Date: Thu, 15 Oct 2026 19:43:45 +0000 Subject: [PATCH 02/10] [IMP] attachment_unindex_content: read index_content from the file The module answers `_index` with False so nothing is stored in `index_content`, and the field is then empty for every reader. Code that needs the value gets a boolean, for instance the website controller that looks a theme color up in the CSS bundle through `asset.css().index_content`: TypeError: expected string or bytes-like object, got 'bool' Keep the column empty, but when the field is fetched and the database has no value, index the file at that moment with the next `_index` of the MRO (the one the module disables) and only put the result in the cache. Nothing is written to the database, searching the field gives the same as before, and a value written by hand is kept. The tests that checked that the field reads False now check the stored column. --- attachment_unindex_content/README.rst | 9 +++ .../models/ir_attachment.py | 28 +++++++ .../readme/DESCRIPTION.md | 8 ++ .../static/description/index.html | 8 ++ .../tests/test_attachment_index.py | 77 ++++++++++++++++++- 5 files changed, 126 insertions(+), 4 deletions(-) diff --git a/attachment_unindex_content/README.rst b/attachment_unindex_content/README.rst index 4e1ef159110..d4016a51941 100644 --- a/attachment_unindex_content/README.rst +++ b/attachment_unindex_content/README.rst @@ -65,6 +65,15 @@ it at ``ir.attachment`` base. Also, after the installation, the ``index_content`` field on attachments already recorded in database will be cleared. +Nothing is stored, but the field is still readable: when the database +has no content for an attachment, reading ``index_content`` indexes the +file at that moment and puts the result in the cache only. Code that +reads the field, as the website does to find a color in the CSS bundle, +keeps working with no copy of the content in the database. The cost is +reading the file of every attachment whose ``index_content`` is +requested. Searching on the field still finds nothing, because the +column stays empty. + .. |SQL Query result showing indexed content| image:: https://user-images.githubusercontent.com/442938/67894113-45d27a80-fb2e-11e9-9a22-ba43d8b444c5.png :target: https://user-images.githubusercontent.com/442938/67894113-45d27a80-fb2e-11e9-9a22-ba43d8b444c5.png diff --git a/attachment_unindex_content/models/ir_attachment.py b/attachment_unindex_content/models/ir_attachment.py index 772e38ce20e..54b37196d8d 100644 --- a/attachment_unindex_content/models/ir_attachment.py +++ b/attachment_unindex_content/models/ir_attachment.py @@ -10,3 +10,31 @@ class IrAttachment(models.Model): @api.model def _index(self, *args, **kwargs): return False + + def _fetch_query(self, query, fields): + """Give ``index_content`` the content of the file when the database has none. + + The content is not stored, so the column is empty, but whoever reads the field + still needs its value: core reads it to look a color up in the CSS bundle of the + website. Read it from the file, as the indexation would have stored it, and only + put it in the cache: nothing is written to the database. + """ + fetched = super()._fetch_query(query, fields) + field = self._fields["index_content"] + if fetched and field in fields: + cache = field._get_cache(self.env) + for attachment in fetched: + if cache.get(attachment.id): + continue + content = self._index_content_from_file(attachment) + if content: + field._update_cache(attachment, content) + return fetched + + def _index_content_from_file(self, attachment): + """Return what the indexation would have stored for ``attachment``.""" + raw = attachment.raw + if not raw: + return False + # Skip this class: it is the one that disables the indexation + return super()._index(raw, attachment.mimetype, checksum=attachment.checksum) diff --git a/attachment_unindex_content/readme/DESCRIPTION.md b/attachment_unindex_content/readme/DESCRIPTION.md index b9a3937441c..36ceee2c777 100644 --- a/attachment_unindex_content/readme/DESCRIPTION.md +++ b/attachment_unindex_content/readme/DESCRIPTION.md @@ -29,3 +29,11 @@ it at `ir.attachment` base. Also, after the installation, the `index_content` field on attachments already recorded in database will be cleared. + +Nothing is stored, but the field is still readable: when the database has +no content for an attachment, reading `index_content` indexes the file at +that moment and puts the result in the cache only. Code that reads the +field, as the website does to find a color in the CSS bundle, keeps working +with no copy of the content in the database. The cost is reading the file +of every attachment whose `index_content` is requested. Searching on the +field still finds nothing, because the column stays empty. diff --git a/attachment_unindex_content/static/description/index.html b/attachment_unindex_content/static/description/index.html index 0a576fc1bff..537778a682e 100644 --- a/attachment_unindex_content/static/description/index.html +++ b/attachment_unindex_content/static/description/index.html @@ -402,6 +402,14 @@

Attachment Unindex Content

it at ir.attachment base.

Also, after the installation, the index_content field on attachments already recorded in database will be cleared.

+

Nothing is stored, but the field is still readable: when the database +has no content for an attachment, reading index_content indexes the +file at that moment and puts the result in the cache only. Code that +reads the field, as the website does to find a color in the CSS bundle, +keeps working with no copy of the content in the database. The cost is +reading the file of every attachment whose index_content is +requested. Searching on the field still finds nothing, because the +column stays empty.

Table of contents

    diff --git a/attachment_unindex_content/tests/test_attachment_index.py b/attachment_unindex_content/tests/test_attachment_index.py index 4d1e1383864..55d45ea2b42 100644 --- a/attachment_unindex_content/tests/test_attachment_index.py +++ b/attachment_unindex_content/tests/test_attachment_index.py @@ -24,6 +24,14 @@ def setUpClass(cls): cls.test_text_content = b"This is a test file content for indexing verification" cls.test_text_b64 = base64.b64encode(cls.test_text_content) + def _stored_index_content(self, attachment): + """Return the ``index_content`` stored, not the one the field reads.""" + attachment.flush_recordset() + self.env.cr.execute( + "SELECT index_content FROM ir_attachment WHERE id = %s", (attachment.id,) + ) + return self.env.cr.fetchone()[0] + def test_attachment_index_content_disabled(self): """Test that index_content is not populated when creating attachment. @@ -39,7 +47,7 @@ def test_attachment_index_content_disabled(self): ) self.assertFalse( - attachment.index_content, + self._stored_index_content(attachment), "index_content should be False (empty) when creating attachment " "because the module disables content indexation", ) @@ -118,7 +126,7 @@ def test_multiple_attachments_no_indexing(self): # Verify that none of the attachments have index_content populated for attachment in attachments: self.assertFalse( - attachment.index_content, + self._stored_index_content(attachment), f"Attachment {attachment.name} should not have indexed content", ) @@ -151,7 +159,7 @@ def test_different_file_types_no_indexing(self): ) self.assertFalse( - attachment.index_content, + self._stored_index_content(attachment), f"Attachment {name} with mimetype {mimetype} should not be indexed", ) @@ -174,4 +182,65 @@ def test_attachment_search_without_index(self): "Should find attachment by name even without content indexing", ) self.assertEqual(found.id, attachment.id) - self.assertFalse(found.index_content, "Found attachment should not be indexed") + self.assertFalse( + self._stored_index_content(found), "Found attachment should not be indexed" + ) + + def test_index_content_is_read_from_the_file(self): + """The field reads the content of the file, which is not stored.""" + attachment = self.Attachment.create( + { + "name": "readable.txt", + "datas": self.test_text_b64, + "mimetype": "text/plain", + } + ) + attachment.invalidate_recordset() + self.assertEqual( + attachment.index_content, self.test_text_content.decode(), "Read the file" + ) + self.assertFalse( + self._stored_index_content(attachment), "Nothing is stored in the database" + ) + + def test_index_content_is_read_from_the_file_for_several_records(self): + attachments = self.Attachment.create( + [ + { + "name": f"readable_{i}.txt", + "raw": f"content number {i}".encode(), + "mimetype": "text/plain", + } + for i in range(3) + ] + ) + attachments.invalidate_recordset() + self.assertEqual( + attachments.mapped("index_content"), + [f"content number {i}" for i in range(3)], + ) + self.assertEqual( + [row["index_content"] for row in attachments.read(["index_content"])], + [f"content number {i}" for i in range(3)], + ) + + def test_index_content_of_an_attachment_without_file_is_empty(self): + attachment = self.Attachment.create( + { + "name": "link", + "type": "url", + "url": "https://example.com", + "mimetype": "text/plain", + } + ) + attachment.invalidate_recordset() + self.assertFalse(attachment.index_content) + + def test_index_content_written_by_hand_is_kept(self): + attachment = self.Attachment.create( + {"name": "ocr.txt", "raw": b"from the file", "mimetype": "text/plain"} + ) + attachment.index_content = "from the ocr" + attachment.flush_recordset() + attachment.invalidate_recordset() + self.assertEqual(attachment.index_content, "from the ocr") From cd19306f0d362d99cae29349b8d6ffe62f5f8692 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 23 Oct 2026 05:13:43 +0000 Subject: [PATCH 03/10] [UPD] Update base_view_inheritance_extension.pot --- .../i18n/base_view_inheritance_extension.pot | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/base_view_inheritance_extension/i18n/base_view_inheritance_extension.pot b/base_view_inheritance_extension/i18n/base_view_inheritance_extension.pot index ecb0b4f08d9..f65b98bbbf8 100644 --- a/base_view_inheritance_extension/i18n/base_view_inheritance_extension.pot +++ b/base_view_inheritance_extension/i18n/base_view_inheritance_extension.pot @@ -18,6 +18,12 @@ msgstr "" msgid "Display Name" msgstr "" +#. module: base_view_inheritance_extension +#. odoo-python +#: code:addons/base_view_inheritance_extension/models/ir_ui_view.py:0 +msgid "Element '%s' cannot be located in parent view" +msgstr "" + #. module: base_view_inheritance_extension #: model:ir.model.fields,field_description:base_view_inheritance_extension.field_ir_ui_view__id msgid "ID" From 316602e021f89da0d3dc33ba9e887ac3e9619fd8 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 23 Oct 2026 05:17:52 +0000 Subject: [PATCH 04/10] [BOT] post-merge updates --- README.md | 2 +- base_view_inheritance_extension/README.rst | 2 +- base_view_inheritance_extension/__manifest__.py | 2 +- base_view_inheritance_extension/static/description/index.html | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 65f1eb90fc9..728d4417244 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ addon | version | maintainers | summary [base_search_fuzzy](base_search_fuzzy/) | 19.0.1.0.0 | | Fuzzy search with the PostgreSQL trigram extension [base_technical_user](base_technical_user/) | 19.0.1.0.0 | | Add a technical user parameter on the company [base_time_window](base_time_window/) | 19.0.1.0.0 | | Base model to handle time windows -[base_view_inheritance_extension](base_view_inheritance_extension/) | 19.0.1.0.0 | hbrunn | Adds more operators for view inheritance +[base_view_inheritance_extension](base_view_inheritance_extension/) | 19.0.1.0.1 | hbrunn | Adds more operators for view inheritance [base_write_diff](base_write_diff/) | 19.0.1.0.0 | | Prevents updates on fields whose values won't change anyway [bus_alt_connection](bus_alt_connection/) | 19.0.1.0.0 | | Needed when using PgBouncer as a connection pooler [database_cleanup](database_cleanup/) | 19.0.1.0.6 | | Database cleanup diff --git a/base_view_inheritance_extension/README.rst b/base_view_inheritance_extension/README.rst index 3bab25802d3..1a175b134c7 100644 --- a/base_view_inheritance_extension/README.rst +++ b/base_view_inheritance_extension/README.rst @@ -11,7 +11,7 @@ Extended view inheritance !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:9089555b5aea01b23ff4a0350d6fd69589d63f1beb4e272ed5a96eebf05b2a81 + !! source digest: sha256:9ed71530bde28eea51dc4bcbb711aed870efc9b5c3ef861adca68e3f129b3a62 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png diff --git a/base_view_inheritance_extension/__manifest__.py b/base_view_inheritance_extension/__manifest__.py index 5219ab7b857..167e019aa9a 100644 --- a/base_view_inheritance_extension/__manifest__.py +++ b/base_view_inheritance_extension/__manifest__.py @@ -3,7 +3,7 @@ # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). { "name": "Extended view inheritance", - "version": "19.0.1.0.0", + "version": "19.0.1.0.1", "development_status": "Mature", "author": "Therp BV,Odoo Community Association (OCA)", "license": "LGPL-3", diff --git a/base_view_inheritance_extension/static/description/index.html b/base_view_inheritance_extension/static/description/index.html index 611aa8047c1..3ed0d5d02b1 100644 --- a/base_view_inheritance_extension/static/description/index.html +++ b/base_view_inheritance_extension/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Extended view inheritance