diff --git a/.copier-answers.yml b/.copier-answers.yml index a406cf7de8..f63257825c 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: v1.34 +_commit: v1.35 _src_path: git+https://github.com/OCA/oca-addons-repo-template additional_ruff_rules: [] ci: GitHub diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f737dac10a..3e45dd26a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,12 @@ exclude: | (?x) # NOT INSTALLABLE ADDONS - ^fs_attachment/| ^fs_attachment_s3/| ^fs_file/| ^fs_folder/| ^fs_folder_demo/| ^fs_folder_ms_drive/| ^fs_image/| - ^fs_storage/| ^fs_storage_ms_drive/| ^image_tag/| ^microsoft_drive_account/| diff --git a/.pylintrc b/.pylintrc index 16996cb36f..f3d017a8f5 100644 --- a/.pylintrc +++ b/.pylintrc @@ -23,21 +23,12 @@ disable=all # config as a blocking check. enable=anomalous-backslash-in-string, - api-one-deprecated, - api-one-multi-together, assignment-from-none, attribute-deprecated, - class-camelcase, dangerous-default-value, - dangerous-view-replace-wo-priority, development-status-allowed, - duplicate-id-csv, duplicate-key, - duplicate-xml-fields, - duplicate-xml-record-id, - eval-referenced, eval-used, - incoherent-interpreter-exec-perm, license-allowed, manifest-author-string, manifest-deprecated-key, @@ -48,56 +39,33 @@ enable=anomalous-backslash-in-string, method-inverse, method-required-super, method-search, - openerp-exception-warning, pointless-statement, pointless-string-statement, print-used, redundant-keyword-arg, - redundant-modulename-xml, reimported, - relative-import, return-in-init, - rst-syntax-error, sql-injection, too-few-format-args, translation-field, translation-required, unreachable, use-vim-comment, - wrong-tabs-instead-of-spaces, - xml-syntax-error, attribute-string-redundant, - character-not-valid-in-resource-link, consider-merging-classes-inherited, context-overridden, - create-user-wo-reset-password, - dangerous-filter-wo-user, - dangerous-qweb-replace-wo-priority, - deprecated-data-xml-node, - deprecated-openerp-xml-node, - duplicate-po-message-definition, except-pass, - file-not-used, invalid-commit, manifest-maintainers-list, - missing-newline-extrafiles, missing-readme, missing-return, odoo-addons-relative-import, - old-api7-method-defined, - po-msgstr-variables, - po-syntax-error, renamed-field-parameter, resource-not-exist, - str-format-used, test-folder-imported, translation-contains-variable, translation-positional-used, - unnecessary-utf8-coding-comment, website-manifest-key-not-valid-uri, - xml-attribute-translatable, - xml-deprecated-qweb-directive, - xml-deprecated-tree-attribute, external-request-timeout, bad-builtin-groupby, category-allowed, diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory index dc9b71ed30..80567de1e7 100644 --- a/.pylintrc-mandatory +++ b/.pylintrc-mandatory @@ -15,21 +15,12 @@ valid-odoo-versions=19.0 disable=all enable=anomalous-backslash-in-string, - api-one-deprecated, - api-one-multi-together, assignment-from-none, attribute-deprecated, - class-camelcase, dangerous-default-value, - dangerous-view-replace-wo-priority, development-status-allowed, - duplicate-id-csv, duplicate-key, - duplicate-xml-fields, - duplicate-xml-record-id, - eval-referenced, eval-used, - incoherent-interpreter-exec-perm, license-allowed, manifest-author-string, manifest-deprecated-key, @@ -40,56 +31,33 @@ enable=anomalous-backslash-in-string, method-inverse, method-required-super, method-search, - openerp-exception-warning, pointless-statement, pointless-string-statement, print-used, redundant-keyword-arg, - redundant-modulename-xml, reimported, - relative-import, return-in-init, - rst-syntax-error, sql-injection, too-few-format-args, translation-field, translation-required, unreachable, use-vim-comment, - wrong-tabs-instead-of-spaces, - xml-syntax-error, attribute-string-redundant, - character-not-valid-in-resource-link, consider-merging-classes-inherited, context-overridden, - create-user-wo-reset-password, - dangerous-filter-wo-user, - dangerous-qweb-replace-wo-priority, - deprecated-data-xml-node, - deprecated-openerp-xml-node, - duplicate-po-message-definition, except-pass, - file-not-used, invalid-commit, manifest-maintainers-list, - missing-newline-extrafiles, missing-readme, missing-return, odoo-addons-relative-import, - old-api7-method-defined, - po-msgstr-variables, - po-syntax-error, renamed-field-parameter, resource-not-exist, - str-format-used, test-folder-imported, translation-contains-variable, translation-positional-used, - unnecessary-utf8-coding-comment, website-manifest-key-not-valid-uri, - xml-attribute-translatable, - xml-deprecated-qweb-directive, - xml-deprecated-tree-attribute, external-request-timeout, bad-builtin-groupby, category-allowed, diff --git a/fs_attachment/README.rst b/fs_attachment/README.rst index 5731638107..edb9092ff6 100644 --- a/fs_attachment/README.rst +++ b/fs_attachment/README.rst @@ -21,13 +21,13 @@ Base Attachment Object Store :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github - :target: https://github.com/OCA/storage/tree/18.0/fs_attachment + :target: https://github.com/OCA/storage/tree/19.0/fs_attachment :alt: OCA/storage .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/storage-18-0/storage-18-0-fs_attachment + :target: https://translation.odoo-community.org/projects/storage-19-0/storage-19-0-fs_attachment :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=18.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -422,7 +422,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -475,6 +475,6 @@ Current `maintainer `__: |maintainer-lmignon| -This module is part of the `OCA/storage `_ project on GitHub. +This module is part of the `OCA/storage `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fs_attachment/__manifest__.py b/fs_attachment/__manifest__.py index b2d4632db7..7777708588 100644 --- a/fs_attachment/__manifest__.py +++ b/fs_attachment/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Attachment Object Store", "summary": "Store attachments on external object store", - "version": "18.0.2.1.0", + "version": "19.0.1.0.0", "author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)", "license": "AGPL-3", "development_status": "Beta", @@ -17,7 +17,7 @@ "views/fs_storage.xml", ], "external_dependencies": {"python": ["python_slugify", "fsspec>=2025.3.0"]}, - "installable": False, + "installable": True, "auto_install": False, "maintainers": ["lmignon"], "pre_init_hook": "pre_init_hook", diff --git a/fs_attachment/models/fs_file_gc.py b/fs_attachment/models/fs_file_gc.py index d10dd77418..3ba7ebf934 100644 --- a/fs_attachment/models/fs_file_gc.py +++ b/fs_attachment/models/fs_file_gc.py @@ -4,7 +4,7 @@ import threading from contextlib import closing, contextmanager -from odoo import api, fields, models +from odoo import api, fields, models, modules from odoo.sql_db import Cursor _logger = logging.getLogger(__name__) @@ -17,13 +17,10 @@ class FsFileGC(models.Model): store_fname = fields.Char("Stored Filename") fs_storage_code = fields.Char("Storage Code") - _sql_constraints = [ - ( - "store_fname_uniq", - "unique (store_fname)", - "The stored filename must be unique!", - ), - ] + _store_fname_uniq = models.Constraint( + "unique (store_fname)", + "The stored filename must be unique!", + ) def _is_test_mode(self) -> bool: """Return True if we are running the tests, so we do not mark files for @@ -31,7 +28,7 @@ def _is_test_mode(self) -> bool: """ return ( getattr(threading.current_thread(), "testing", False) - or self.env.registry.in_test_mode() + or modules.module.current_test ) @contextmanager @@ -101,7 +98,7 @@ def _gc_files(self) -> None: # the LOCK statement will wait until those concurrent transactions end. # But this transaction will not see the new attachements if it has done # other requests before the LOCK (like the method _storage() above). - cr = self._cr + cr = self.env.cr cr.commit() # pylint: disable=invalid-commit # prevent all concurrent updates on ir_attachment and fs_file_gc @@ -120,12 +117,12 @@ def _gc_files(self) -> None: def _gc_files_unsafe(self) -> None: # get the list of fs.storage codes that must be autovacuumed codes = ( - self.env["fs.storage"].search([]).filtered("autovacuum_gc").mapped("code") + self.env["fs.storage"].search([]).filtered("autovacuum_gc").mapped("code") # pylint: disable=no-search-all ) if not codes: return # we process by batch of storage codes. - self._cr.execute( + self.env.cr.execute( """ SELECT fs_storage_code, @@ -145,7 +142,7 @@ def _gc_files_unsafe(self) -> None: """, (tuple(codes),), ) - for code, store_fnames in self._cr.fetchall(): + for code, store_fnames in self.env.cr.fetchall(): self.env["fs.storage"].get_by_code(code) fs = self.env["fs.storage"].get_fs_by_code(code) for store_fname in store_fnames: @@ -156,7 +153,7 @@ def _gc_files_unsafe(self) -> None: _logger.debug("Failed to remove file %s", store_fname) # delete the records from the table fs_file_gc - self._cr.execute( + self.env.cr.execute( """ DELETE FROM fs_file_gc diff --git a/fs_attachment/models/fs_storage.py b/fs_attachment/models/fs_storage.py index 4bfb086484..f451ea25c8 100644 --- a/fs_attachment/models/fs_storage.py +++ b/fs_attachment/models/fs_storage.py @@ -3,7 +3,7 @@ from __future__ import annotations -from odoo import _, api, fields, models, tools +from odoo import api, fields, models, tools from odoo.exceptions import ValidationError from odoo.tools.safe_eval import const_eval @@ -82,10 +82,10 @@ class FsStorage(models.Model): def _check_use_as_default_for_attachments(self): # constrains are checked in python since values can be provided by # the server environment - defaults = self.search([]).filtered("use_as_default_for_attachments") + defaults = self.search([]).filtered("use_as_default_for_attachments") # pylint: disable=no-search-all if len(defaults) > 1: raise ValidationError( - _("Only one storage can be used as default for attachments") + self.env._("Only one storage can be used as default for attachments") ) @property @@ -165,7 +165,7 @@ def _check_force_db_for_default_attachment_rules(self): continue if not rec.use_as_default_for_attachments: raise ValidationError( - _( + self.env._( "The force_db_for_default_attachment_rules can only be set " "if the storage is used as default for attachments." ) @@ -174,7 +174,7 @@ def _check_force_db_for_default_attachment_rules(self): const_eval(rec.force_db_for_default_attachment_rules) except (SyntaxError, TypeError, ValueError) as e: raise ValidationError( - _( + self.env._( "The force_db_for_default_attachment_rules is not a valid " "python dict." ) @@ -184,7 +184,7 @@ def _check_force_db_for_default_attachment_rules(self): @tools.ormcache() def get_storage_code_for_attachments_fallback(self): storages = ( - self.sudo() + self.sudo() # pylint: disable=no-search-all .search([]) .filtered_domain([("use_as_default_for_attachments", "=", True)]) ) diff --git a/fs_attachment/models/ir_attachment.py b/fs_attachment/models/ir_attachment.py index 6bafc7dedb..a87879c763 100644 --- a/fs_attachment/models/ir_attachment.py +++ b/fs_attachment/models/ir_attachment.py @@ -18,9 +18,9 @@ from slugify import slugify # pylint: disable=missing-manifest-dependency import odoo -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import AccessError, UserError -from odoo.osv.expression import AND, OR, normalize_domain +from odoo.fields import Domain from .strtobool import strtobool @@ -169,9 +169,9 @@ def _store_in_db_instead_of_object_storage_domain(self): for mimetype_key, limit in storage_config.items(): part = [("mimetype", "=like", f"{mimetype_key}%")] if limit: - part = AND([part, [("file_size", "<=", limit)]]) + part = Domain.AND([part, [("file_size", "<=", limit)]]) # OR simplifies to [(1, '=', 1)] if a domain being OR'ed is empty - domain = OR([domain, part]) if domain else part + domain = Domain.OR([domain, part]) if domain else part return domain def _store_in_db_instead_of_object_storage(self, data, mimetype): @@ -224,22 +224,29 @@ def _store_in_db_instead_of_object_storage(self, data, mimetype): return False def _get_datas_related_values(self, data, mimetype): + values = super( + IrAttachment, self.with_context(mimetype=mimetype) + )._get_datas_related_values(data, mimetype) storage = self.env.context.get("storage_location") or self._storage() if data and storage in self._get_storage_codes(): if self._store_in_db_instead_of_object_storage(data, mimetype): - # compute the fields that depend on datas - bin_data = data - values = { - "file_size": len(bin_data), - "checksum": self._compute_checksum(bin_data), - "index_content": self._index(bin_data, mimetype), - "store_fname": False, - "db_datas": data, - } - return values - return super( - IrAttachment, self.with_context(mimetype=mimetype) - )._get_datas_related_values(data, mimetype) + # Force storing data in the database, overriding the filestore logic. + values.update( + { + "store_fname": False, + "db_datas": data, + } + ) + else: + # Uses the full object storage path; standard Odoo uses a relative path. + path = self._get_fs_path(storage, data) + values.update( + { + "store_fname": f"{storage}://{path}", + "db_datas": False, + } + ) + return values ########################################################### # Odoo methods that we override to use the object storage # @@ -306,7 +313,7 @@ def write(self, vals): vals["mimetype"] = mimetypes[0] else: raise UserError( - _( + self.env._( "You can't write on multiple attachments with different " "mimetypes at the same time." ) @@ -697,9 +704,10 @@ def _move_attachment_to_store(self): self.ensure_one() _logger.info("inspecting attachment %s (%d)", self.name, self.id) fname = self.store_fname - storage = fname.partition("://")[0] - if self._is_storage_disabled(storage): - fname = False + if fname: + storage = fname.partition("://")[0] + if self._is_storage_disabled(storage): + fname = False if fname: # migrating from filesystem filestore # or from the old 'store_fname' without the bucket name @@ -723,7 +731,9 @@ def _move_attachment_to_store(self): @api.model def force_storage(self): if not self.env["res.users"].browse(self.env.uid)._is_admin(): - raise AccessError(_("Only administrators can execute this action.")) + raise AccessError( + self.env._("Only administrators can execute this action.") + ) location = self.env.context.get("storage_location") or self._storage() if location not in self._get_storage_codes(): return super().force_storage() @@ -762,19 +772,22 @@ def force_storage_to_db_for_special_fields( ) return - domain = AND( + domain = Domain.AND( ( - normalize_domain( + Domain.AND( [ - ("store_fname", "=like", f"{storage}://%"), + Domain("store_fname", "=like", f"{storage}://%"), # for res_field, see comment in # _force_storage_to_object_storage - "|", - ("res_field", "=", False), - ("res_field", "!=", False), + Domain.OR( + [ + Domain("res_field", "=", False), + Domain("res_field", "!=", False), + ] + ), ] ), - normalize_domain(self._store_in_db_instead_of_object_storage_domain()), + Domain(self._store_in_db_instead_of_object_storage_domain()), ) ) diff --git a/fs_attachment/readme/CONTRIBUTORS.md b/fs_attachment/readme/CONTRIBUTORS.md index 071273090e..bc082ea0ba 100644 --- a/fs_attachment/readme/CONTRIBUTORS.md +++ b/fs_attachment/readme/CONTRIBUTORS.md @@ -1,4 +1,4 @@ -* Thierry Ducrest \<\> +* Thierry Ducrest \<\> * Guewen Baconnier \<\> * Julien Coux \<\> * Akim Juillerat \<\> diff --git a/fs_attachment/static/description/index.html b/fs_attachment/static/description/index.html index 05fa1ca9d9..0c241280cf 100644 --- a/fs_attachment/static/description/index.html +++ b/fs_attachment/static/description/index.html @@ -374,7 +374,7 @@

Base Attachment Object Store

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:ce5c43b22c654c05f7c0b6c6b919001cb4a4c77d4031841a7a9acc02a9b2d1e0 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

In some cases, you need to store attachment in another system that the Odoo’s filestore. For example, when your deployment is based on a multi-server architecture to ensure redundancy and scalability, your @@ -772,7 +772,7 @@

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -815,7 +815,7 @@

Maintainers

promote its widespread use.

Current maintainer:

lmignon

-

This module is part of the OCA/storage project on GitHub.

+

This module is part of the OCA/storage project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

diff --git a/fs_attachment/tests/common.py b/fs_attachment/tests/common.py index 076717a90b..9f3c36f180 100644 --- a/fs_attachment/tests/common.py +++ b/fs_attachment/tests/common.py @@ -4,6 +4,7 @@ import shutil import tempfile +from odoo.fields import Command from odoo.tests.common import TransactionCase @@ -13,6 +14,13 @@ def setUpClass(cls): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) temp_dir = tempfile.mkdtemp() + cls.default_backend = cls.env["fs.storage"].create( + { + "name": "Odoo Filesystem Backend", + "protocol": "odoofs", + "code": "odoofs", + } + ) cls.temp_backend = cls.env["fs.storage"].create( { "name": "Temp FS Storage", @@ -33,6 +41,20 @@ def setUpClass(cls): cls.temp_dir = temp_dir cls.gc_file_model = cls.env["fs.file.gc"] cls.ir_attachment_model = cls.env["ir.attachment"] + cls.demo_user = ( + cls.env["res.users"] + .with_context(no_reset_password=True) + .create( + { + "name": "Test User", + "login": "demo", + "password": "demo", + "email": "test@yourcompany.com", + "company_id": cls.env.ref("base.main_company").id, + "group_ids": [Command.link(cls.env.ref("base.group_user").id)], + } + ) + ) @cls.addClassCleanup def cleanup_tempdir(): diff --git a/fs_attachment/tests/test_fs_attachment.py b/fs_attachment/tests/test_fs_attachment.py index 22c186ecac..a6579c20da 100644 --- a/fs_attachment/tests/test_fs_attachment.py +++ b/fs_attachment/tests/test_fs_attachment.py @@ -3,6 +3,7 @@ import os from unittest import mock +from odoo.fields import Domain from odoo.tools import mute_logger from .common import MyException, TestFSAttachmentCommon @@ -389,21 +390,20 @@ def test_storage_use_filename_obfuscation(self): self.assertEqual(attachment.mimetype, "text/plain") def test_create_attachments_basic_user(self): - demo_user = self.env.ref("base.user_demo") - demo_partner = self.env.ref("base.partner_demo") self.temp_backend.use_as_default_for_attachments = True # Ensure basic access group_user = self.env.ref("base.group_user") group_partner_manager = self.env.ref("base.group_partner_manager") - demo_user.write( - {"groups_id": [(6, 0, [group_user.id, group_partner_manager.id])]} + self.demo_user.write( + {"group_ids": [(6, 0, [group_user.id, group_partner_manager.id])]} ) + demo_partner = self.demo_user.partner_id # Create basic attachment - self.ir_attachment_model.with_user(demo_user).create( + self.ir_attachment_model.with_user(self.demo_user).create( {"name": "test.txt", "raw": b"content"} ) # Create attachment related to model - self.ir_attachment_model.with_user(demo_user).create( + self.ir_attachment_model.with_user(self.demo_user).create( { "name": "test.txt", "raw": b"content", @@ -415,7 +415,7 @@ def test_create_attachments_basic_user(self): partner_image_field = self.env["ir.model.fields"].search( [("model", "=", "res.partner"), ("name", "=", "image1920")] ) - self.ir_attachment_model.with_user(demo_user).create( + self.ir_attachment_model.with_user(self.demo_user).create( { "name": "test.txt", "raw": b"content", @@ -479,11 +479,15 @@ def test_store_in_db_instead_of_object_storage_domain(self): ) self.assertEqual( self.env["ir.attachment"]._store_in_db_instead_of_object_storage_domain(), - [ - "|", - ("mimetype", "=like", "text/plain%"), - "&", - ("mimetype", "=like", "image/png%"), - ("file_size", "<=", 100), - ], + Domain.OR( + [ + Domain("mimetype", "=like", "text/plain%"), + Domain.AND( + [ + Domain("mimetype", "=like", "image/png%"), + Domain("file_size", "<=", 100), + ] + ), + ] + ), ) diff --git a/fs_attachment/tests/test_fs_storage.py b/fs_attachment/tests/test_fs_storage.py index 5f6835dbbe..f947f455d4 100644 --- a/fs_attachment/tests/test_fs_storage.py +++ b/fs_attachment/tests/test_fs_storage.py @@ -7,12 +7,6 @@ class TestFsStorage(TestFSAttachmentCommon): - @classmethod - def setUpClass(cls): - res = super().setUpClass() - cls.default_backend = cls.env.ref("fs_storage.fs_storage_demo") - return res - def test_force_model_create_attachment(self): """ Force 'res.partner' model to temp_backend diff --git a/fs_attachment/tests/test_stream.py b/fs_attachment/tests/test_stream.py index 38ee7a4c0f..69f42bf9f1 100644 --- a/fs_attachment/tests/test_stream.py +++ b/fs_attachment/tests/test_stream.py @@ -170,7 +170,7 @@ def test_serving_field_image(self): demo_partner = self.env.ref("base.partner_demo") demo_partner.with_context( storage_location=self.temp_backend.code, - ).write({"image_128": base64.encodebytes(self._create_image(128, 128))}) + ).write({"image_128": base64.encodebytes(self.env.create_image(128, 128))}) url = f"/web/image/{demo_partner._name}/{demo_partner.id}/image_128" res = self.assertDownload( url, diff --git a/fs_storage/README.rst b/fs_storage/README.rst index 95bbcb6801..35ebd9f687 100644 --- a/fs_storage/README.rst +++ b/fs_storage/README.rst @@ -21,13 +21,13 @@ Filesystem Storage Backend :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github - :target: https://github.com/OCA/storage/tree/18.0/fs_storage + :target: https://github.com/OCA/storage/tree/19.0/fs_storage :alt: OCA/storage .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/storage-18-0/storage-18-0-fs_storage + :target: https://translation.odoo-community.org/projects/storage-19-0/storage-19-0-fs_storage :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=18.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -287,7 +287,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -305,6 +305,7 @@ Contributors - Laurent Mignon - Sébastien BEAU - Marie Lejeune +- Julien Coux Maintainers ----------- @@ -319,6 +320,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -This module is part of the `OCA/storage `_ project on GitHub. +This module is part of the `OCA/storage `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fs_storage/__manifest__.py b/fs_storage/__manifest__.py index 401e5748eb..d6bb9bf5f4 100644 --- a/fs_storage/__manifest__.py +++ b/fs_storage/__manifest__.py @@ -5,19 +5,18 @@ { "name": "Filesystem Storage Backend", "summary": "Implement the concept of Storage with amazon S3, sftp...", - "version": "18.0.2.0.1", + "version": "19.0.1.0.0", "category": "FS Storage", "website": "https://github.com/OCA/storage", "author": " ACSONE SA/NV, Odoo Community Association (OCA)", "license": "LGPL-3", "development_status": "Beta", - "installable": False, "depends": ["base", "base_sparse_field", "server_environment"], "data": [ "views/fs_storage_view.xml", "security/ir.model.access.csv", "wizards/fs_test_connection.xml", ], - "demo": ["demo/fs_storage_demo.xml"], "external_dependencies": {"python": ["fsspec>=2024.5.0"]}, + "installable": True, } diff --git a/fs_storage/demo/fs_storage_demo.xml b/fs_storage/demo/fs_storage_demo.xml deleted file mode 100644 index 0917d06221..0000000000 --- a/fs_storage/demo/fs_storage_demo.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - Odoo Filesystem Backend - odoofs - odoofs - - diff --git a/fs_storage/models/fs_storage.py b/fs_storage/models/fs_storage.py index 14941c540b..3e516be4f7 100644 --- a/fs_storage/models/fs_storage.py +++ b/fs_storage/models/fs_storage.py @@ -14,7 +14,7 @@ import fsspec -from odoo import _, api, fields, models, tools +from odoo import api, fields, models, tools from odoo.exceptions import ValidationError from odoo.addons.base_sparse_field.models.fields import Serialized @@ -189,13 +189,10 @@ def __init__(self, env, ids=(), prefetch_ids=()): "* List File : List all files from root directory", ) - _sql_constraints = [ - ( - "code_uniq", - "unique(code)", - "The code must be unique", - ), - ] + _uniq_code = models.Constraint( + "unique(code)", + "The code must be unique", + ) _server_env_section_name_field = "code" @@ -210,7 +207,7 @@ def _check_model_xmlid_storage_unique(self): xmlids = rec.model_xmlids.split(",") for xmlid in xmlids: other_storages = ( - self.env["fs.storage"] + self.env["fs.storage"] # pylint: disable=no-search-all .search([]) .filtered_domain( [ @@ -221,11 +218,12 @@ def _check_model_xmlid_storage_unique(self): ) if other_storages: raise ValidationError( - _( + self.env._( "Model %(model)s already stored in another " - "FS storage ('%(other_storage)s')" + "FS storage ('%(other_storage)s')", + model=xmlid, + other_storage=other_storages[0].name, ) - % {"model": xmlid, "other_storage": other_storages[0].name} ) @api.constrains("field_xmlids") @@ -239,7 +237,7 @@ def _check_field_xmlid_storage_unique(self): xmlids = rec.field_xmlids.split(",") for xmlid in xmlids: other_storages = ( - self.env["fs.storage"] + self.env["fs.storage"] # pylint: disable=no-search-all .search([]) .filtered_domain( [ @@ -250,18 +248,19 @@ def _check_field_xmlid_storage_unique(self): ) if other_storages: raise ValidationError( - _( + self.env._( "Field %(field)s already stored in another " - "FS storage ('%(other_storage)s')" + "FS storage ('%(other_storage)s')", + field=xmlid, + other_storage=other_storages[0].name, ) - % {"field": xmlid, "other_storage": other_storages[0].name} ) @api.model def _get_check_connection_method_selection(self): return [ - ("marker_file", _("Create Marker file")), - ("ls", _("List File")), + ("marker_file", self.env._("Create Marker file")), + ("ls", self.env._("List File")), ] @property @@ -338,7 +337,7 @@ def get_storage_code_by_model_field(self, model_name, field_name=None): ) if field: storage = ( - self.env["fs.storage"] + self.env["fs.storage"] # pylint: disable=no-search-all .sudo() .search([]) .filtered_domain([("field_ids", "in", [field.id])]) @@ -353,7 +352,7 @@ def get_storage_code_by_model_field(self, model_name, field_name=None): ) if model: storage = ( - self.env["fs.storage"] + self.env["fs.storage"] # pylint: disable=no-search-all .sudo() .search([]) .filtered_domain([("model_ids", "in", [model.id])]) @@ -399,7 +398,9 @@ def _check_options(self) -> None: try: json.loads(rec.options or "{}") except Exception as e: - raise ValidationError(_("The options must be a valid JSON")) from e + raise ValidationError( + self.env._("The options must be a valid JSON") + ) from e @api.depends("options") def _compute_json_options(self) -> None: @@ -703,11 +704,11 @@ def action_test_config(self): def _test_config(self, connection_method): try: self._check_connection(self.fs, connection_method) - title = _("Connection Test Succeeded!") - message = _("Everything seems properly set up!") + title = self.env._("Connection Test Succeeded!") + message = self.env._("Everything seems properly set up!") msg_type = "success" except Exception as err: - title = _("Connection Test Failed!") + title = self.env._("Connection Test Failed!") message = str(err) msg_type = "danger" return { diff --git a/fs_storage/readme/CONTRIBUTORS.md b/fs_storage/readme/CONTRIBUTORS.md index 139cad9973..0ec48212d3 100644 --- a/fs_storage/readme/CONTRIBUTORS.md +++ b/fs_storage/readme/CONTRIBUTORS.md @@ -1,3 +1,4 @@ - Laurent Mignon \<\> - Sébastien BEAU \<\> - Marie Lejeune \<\> +- Julien Coux \<\> diff --git a/fs_storage/static/description/index.html b/fs_storage/static/description/index.html index 13eba13760..d72205ab97 100644 --- a/fs_storage/static/description/index.html +++ b/fs_storage/static/description/index.html @@ -374,7 +374,7 @@

Filesystem Storage Backend

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:bef82e0e40abd3cfd18d50b3c781d338527ee3bb71f926647c1b5ceeab1fc988 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: LGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

Beta License: LGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

This addon is a technical addon that allows you to define filesystem like storage for your data. It’s used by other addons to store their data in a transparent way into different kind of storages.

@@ -644,7 +644,7 @@

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -661,6 +661,7 @@

Contributors

  • Laurent Mignon <laurent.mignon@acsone.eu>
  • Sébastien BEAU <sebastien.beau@akretion.com>
  • Marie Lejeune <marie.lejeune@acsone.eu>
  • +
  • Julien Coux <julien.coux@camptocamp.com>
  • @@ -672,7 +673,7 @@

    Maintainers

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    -

    This module is part of the OCA/storage project on GitHub.

    +

    This module is part of the OCA/storage project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    diff --git a/fs_storage/tests/common.py b/fs_storage/tests/common.py index 03dd68762f..76e73eb8f8 100644 --- a/fs_storage/tests/common.py +++ b/fs_storage/tests/common.py @@ -6,6 +6,7 @@ import tempfile from unittest import mock +from odoo.fields import Command from odoo.tests.common import TransactionCase from ..models.fs_storage import FSStorage @@ -16,12 +17,31 @@ class TestFSStorageCase(TransactionCase): def setUpClass(cls): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - cls.backend: FSStorage = cls.env.ref("fs_storage.fs_storage_demo") + cls.backend: FSStorage = cls.env["fs.storage"].create( + { + "name": "Odoo Filesystem Backend", + "protocol": "odoofs", + "code": "odoofs", + } + ) cls.backend.json_options = {"target_options": {"auto_mkdir": "True"}} cls.filedata = base64.b64encode(b"This is a simple file") cls.filename = "test_file.txt" cls.case_with_subdirectory = "subdirectory/here" - cls.demo_user = cls.env.ref("base.user_demo") + cls.demo_user = ( + cls.env["res.users"] + .with_context(no_reset_password=True) + .create( + { + "name": "Test User", + "login": "demo", + "password": "demo", + "email": "test@yourcompany.com", + "company_id": cls.env.ref("base.main_company").id, + "group_ids": [Command.link(cls.env.ref("base.group_user").id)], + } + ) + ) cls.temp_dir = tempfile.mkdtemp() def setUp(self): diff --git a/fs_storage/tests/test_fs_storage.py b/fs_storage/tests/test_fs_storage.py index 3d23512a4c..7ef6859a97 100644 --- a/fs_storage/tests/test_fs_storage.py +++ b/fs_storage/tests/test_fs_storage.py @@ -82,7 +82,12 @@ def test_ensure_one_fs_by_record(self): for i in range(4): backend_ids.append( self.backend.create( - {"name": f"name{i}", "directory_path": f"{i}", "code": f"code{i}"} + { + "name": f"name{i}", + "directory_path": f"{i}", + "code": f"code{i}", + "protocol": "odoofs", + } ).id ) records = self.backend.browse(backend_ids) @@ -227,7 +232,6 @@ def test_constraint_unique_storage_model(self): A given model can be linked to a unique storage """ self.backend.model_xmlids = "base.model_res_partner,base.model_ir_attachment" - self.env.ref("fs_storage.fs_storage_demo") with self.assertRaises(ValidationError): self.copy_backend.model_xmlids = "base.model_res_partner" diff --git a/requirements.txt b/requirements.txt index aeccb5304f..35567e0740 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,4 @@ # generated from manifests external_dependencies -boto3 fsspec>=2024.5.0 -fsspec>=2025.0.0 fsspec>=2025.3.0 -fsspec[s3] -msgraphfs -paramiko -pyftpdlib python_slugify diff --git a/test-requirements.txt b/test-requirements.txt index e63f876677..c9666736c7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,3 +2,6 @@ odoo_test_helper requests_mock vcrpy-unittest s3fs>=2025.3.0 + +odoo-addon-server_environment @ git+https://github.com/OCA/server-env@refs/pull/247/head#subdirectory=server_environment +odoo-addon-fs_storage @ git+https://github.com/OCA/storage@refs/pull/524/head#subdirectory=fs_storage