Skip to content

Commit d81a02c

Browse files
committed
Merge PR #544 into 19.0
Signed-off-by lmignon
2 parents 0e1b3c4 + ba8a6ae commit d81a02c

File tree

14 files changed

+134
-103
lines changed

14 files changed

+134
-103
lines changed

.pre-commit-config.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
exclude: |
22
(?x)
33
# NOT INSTALLABLE ADDONS
4-
^fs_attachment/|
54
^fs_attachment_s3/|
65
^fs_file/|
76
^fs_folder/|

fs_attachment/README.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ Base Attachment Object Store
2121
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
2222
:alt: License: AGPL-3
2323
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
24-
:target: https://github.com/OCA/storage/tree/18.0/fs_attachment
24+
:target: https://github.com/OCA/storage/tree/19.0/fs_attachment
2525
:alt: OCA/storage
2626
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
27-
:target: https://translation.odoo-community.org/projects/storage-18-0/storage-18-0-fs_attachment
27+
:target: https://translation.odoo-community.org/projects/storage-19-0/storage-19-0-fs_attachment
2828
:alt: Translate me on Weblate
2929
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
30-
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=18.0
30+
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=19.0
3131
:alt: Try me on Runboat
3232

3333
|badge1| |badge2| |badge3| |badge4| |badge5|
@@ -422,7 +422,7 @@ Bug Tracker
422422
Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
423423
In case of trouble, please check there if your issue has already been reported.
424424
If you spotted it first, help us to smash it by providing a detailed and welcomed
425-
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20fs_attachment%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
425+
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20fs_attachment%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
426426

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

@@ -475,6 +475,6 @@ Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
475475

476476
|maintainer-lmignon|
477477

478-
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/18.0/fs_attachment>`_ project on GitHub.
478+
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/19.0/fs_attachment>`_ project on GitHub.
479479

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

fs_attachment/__manifest__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
{
66
"name": "Base Attachment Object Store",
77
"summary": "Store attachments on external object store",
8-
"version": "18.0.2.1.0",
8+
"version": "19.0.1.0.0",
99
"author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)",
1010
"license": "AGPL-3",
1111
"development_status": "Beta",
@@ -17,7 +17,7 @@
1717
"views/fs_storage.xml",
1818
],
1919
"external_dependencies": {"python": ["python_slugify", "fsspec>=2025.3.0"]},
20-
"installable": False,
20+
"installable": True,
2121
"auto_install": False,
2222
"maintainers": ["lmignon"],
2323
"pre_init_hook": "pre_init_hook",

fs_attachment/models/fs_file_gc.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import threading
55
from contextlib import closing, contextmanager
66

7-
from odoo import api, fields, models
7+
from odoo import api, fields, models, modules
88
from odoo.sql_db import Cursor
99

1010
_logger = logging.getLogger(__name__)
@@ -17,21 +17,18 @@ class FsFileGC(models.Model):
1717
store_fname = fields.Char("Stored Filename")
1818
fs_storage_code = fields.Char("Storage Code")
1919

20-
_sql_constraints = [
21-
(
22-
"store_fname_uniq",
23-
"unique (store_fname)",
24-
"The stored filename must be unique!",
25-
),
26-
]
20+
_store_fname_uniq = models.Constraint(
21+
"unique (store_fname)",
22+
"The stored filename must be unique!",
23+
)
2724

2825
def _is_test_mode(self) -> bool:
2926
"""Return True if we are running the tests, so we do not mark files for
3027
garbage collection into a separate transaction.
3128
"""
3229
return (
3330
getattr(threading.current_thread(), "testing", False)
34-
or self.env.registry.in_test_mode()
31+
or modules.module.current_test
3532
)
3633

3734
@contextmanager
@@ -101,7 +98,7 @@ def _gc_files(self) -> None:
10198
# the LOCK statement will wait until those concurrent transactions end.
10299
# But this transaction will not see the new attachements if it has done
103100
# other requests before the LOCK (like the method _storage() above).
104-
cr = self._cr
101+
cr = self.env.cr
105102
cr.commit() # pylint: disable=invalid-commit
106103

107104
# prevent all concurrent updates on ir_attachment and fs_file_gc
@@ -120,12 +117,12 @@ def _gc_files(self) -> None:
120117
def _gc_files_unsafe(self) -> None:
121118
# get the list of fs.storage codes that must be autovacuumed
122119
codes = (
123-
self.env["fs.storage"].search([]).filtered("autovacuum_gc").mapped("code")
120+
self.env["fs.storage"].search([]).filtered("autovacuum_gc").mapped("code") # pylint: disable=no-search-all
124121
)
125122
if not codes:
126123
return
127124
# we process by batch of storage codes.
128-
self._cr.execute(
125+
self.env.cr.execute(
129126
"""
130127
SELECT
131128
fs_storage_code,
@@ -145,7 +142,7 @@ def _gc_files_unsafe(self) -> None:
145142
""",
146143
(tuple(codes),),
147144
)
148-
for code, store_fnames in self._cr.fetchall():
145+
for code, store_fnames in self.env.cr.fetchall():
149146
self.env["fs.storage"].get_by_code(code)
150147
fs = self.env["fs.storage"].get_fs_by_code(code)
151148
for store_fname in store_fnames:
@@ -156,7 +153,7 @@ def _gc_files_unsafe(self) -> None:
156153
_logger.debug("Failed to remove file %s", store_fname)
157154

158155
# delete the records from the table fs_file_gc
159-
self._cr.execute(
156+
self.env.cr.execute(
160157
"""
161158
DELETE FROM
162159
fs_file_gc

fs_attachment/models/fs_storage.py

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

44
from __future__ import annotations
55

6-
from odoo import _, api, fields, models, tools
6+
from odoo import api, fields, models, tools
77
from odoo.exceptions import ValidationError
88
from odoo.tools.safe_eval import const_eval
99

@@ -82,10 +82,10 @@ class FsStorage(models.Model):
8282
def _check_use_as_default_for_attachments(self):
8383
# constrains are checked in python since values can be provided by
8484
# the server environment
85-
defaults = self.search([]).filtered("use_as_default_for_attachments")
85+
defaults = self.search([]).filtered("use_as_default_for_attachments") # pylint: disable=no-search-all
8686
if len(defaults) > 1:
8787
raise ValidationError(
88-
_("Only one storage can be used as default for attachments")
88+
self.env._("Only one storage can be used as default for attachments")
8989
)
9090

9191
@property
@@ -165,7 +165,7 @@ def _check_force_db_for_default_attachment_rules(self):
165165
continue
166166
if not rec.use_as_default_for_attachments:
167167
raise ValidationError(
168-
_(
168+
self.env._(
169169
"The force_db_for_default_attachment_rules can only be set "
170170
"if the storage is used as default for attachments."
171171
)
@@ -174,7 +174,7 @@ def _check_force_db_for_default_attachment_rules(self):
174174
const_eval(rec.force_db_for_default_attachment_rules)
175175
except (SyntaxError, TypeError, ValueError) as e:
176176
raise ValidationError(
177-
_(
177+
self.env._(
178178
"The force_db_for_default_attachment_rules is not a valid "
179179
"python dict."
180180
)
@@ -184,7 +184,7 @@ def _check_force_db_for_default_attachment_rules(self):
184184
@tools.ormcache()
185185
def get_storage_code_for_attachments_fallback(self):
186186
storages = (
187-
self.sudo()
187+
self.sudo() # pylint: disable=no-search-all
188188
.search([])
189189
.filtered_domain([("use_as_default_for_attachments", "=", True)])
190190
)

fs_attachment/models/ir_attachment.py

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
from contextlib import closing, contextmanager
1414
from pathlib import Path
1515

16-
import fsspec # pylint: disable=missing-manifest-dependency
16+
import fsspec
1717
import psycopg2
18-
from slugify import slugify # pylint: disable=missing-manifest-dependency
18+
from slugify import slugify
1919

2020
import odoo
21-
from odoo import _, api, fields, models
21+
from odoo import api, fields, models
2222
from odoo.exceptions import AccessError, UserError
23-
from odoo.osv.expression import AND, OR, normalize_domain
23+
from odoo.fields import Domain
2424

2525
from .strtobool import strtobool
2626

@@ -169,9 +169,9 @@ def _store_in_db_instead_of_object_storage_domain(self):
169169
for mimetype_key, limit in storage_config.items():
170170
part = [("mimetype", "=like", f"{mimetype_key}%")]
171171
if limit:
172-
part = AND([part, [("file_size", "<=", limit)]])
172+
part = Domain.AND([part, [("file_size", "<=", limit)]])
173173
# OR simplifies to [(1, '=', 1)] if a domain being OR'ed is empty
174-
domain = OR([domain, part]) if domain else part
174+
domain = Domain.OR([domain, part]) if domain else part
175175
return domain
176176

177177
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):
224224
return False
225225

226226
def _get_datas_related_values(self, data, mimetype):
227+
values = super(
228+
IrAttachment, self.with_context(mimetype=mimetype)
229+
)._get_datas_related_values(data, mimetype)
227230
storage = self.env.context.get("storage_location") or self._storage()
228231
if data and storage in self._get_storage_codes():
229232
if self._store_in_db_instead_of_object_storage(data, mimetype):
230-
# compute the fields that depend on datas
231-
bin_data = data
232-
values = {
233-
"file_size": len(bin_data),
234-
"checksum": self._compute_checksum(bin_data),
235-
"index_content": self._index(bin_data, mimetype),
236-
"store_fname": False,
237-
"db_datas": data,
238-
}
239-
return values
240-
return super(
241-
IrAttachment, self.with_context(mimetype=mimetype)
242-
)._get_datas_related_values(data, mimetype)
233+
# Force storing data in the database, overriding the filestore logic.
234+
values.update(
235+
{
236+
"store_fname": False,
237+
"db_datas": data,
238+
}
239+
)
240+
else:
241+
# Uses the full object storage path; standard Odoo uses a relative path.
242+
path = self._get_fs_path(storage, data)
243+
values.update(
244+
{
245+
"store_fname": f"{storage}://{path}",
246+
"db_datas": False,
247+
}
248+
)
249+
return values
243250

244251
###########################################################
245252
# Odoo methods that we override to use the object storage #
@@ -306,7 +313,7 @@ def write(self, vals):
306313
vals["mimetype"] = mimetypes[0]
307314
else:
308315
raise UserError(
309-
_(
316+
self.env._(
310317
"You can't write on multiple attachments with different "
311318
"mimetypes at the same time."
312319
)
@@ -697,9 +704,10 @@ def _move_attachment_to_store(self):
697704
self.ensure_one()
698705
_logger.info("inspecting attachment %s (%d)", self.name, self.id)
699706
fname = self.store_fname
700-
storage = fname.partition("://")[0]
701-
if self._is_storage_disabled(storage):
702-
fname = False
707+
if fname:
708+
storage = fname.partition("://")[0]
709+
if self._is_storage_disabled(storage):
710+
fname = False
703711
if fname:
704712
# migrating from filesystem filestore
705713
# or from the old 'store_fname' without the bucket name
@@ -723,7 +731,9 @@ def _move_attachment_to_store(self):
723731
@api.model
724732
def force_storage(self):
725733
if not self.env["res.users"].browse(self.env.uid)._is_admin():
726-
raise AccessError(_("Only administrators can execute this action."))
734+
raise AccessError(
735+
self.env._("Only administrators can execute this action.")
736+
)
727737
location = self.env.context.get("storage_location") or self._storage()
728738
if location not in self._get_storage_codes():
729739
return super().force_storage()
@@ -762,19 +772,22 @@ def force_storage_to_db_for_special_fields(
762772
)
763773
return
764774

765-
domain = AND(
775+
domain = Domain.AND(
766776
(
767-
normalize_domain(
777+
Domain.AND(
768778
[
769-
("store_fname", "=like", f"{storage}://%"),
779+
Domain("store_fname", "=like", f"{storage}://%"),
770780
# for res_field, see comment in
771781
# _force_storage_to_object_storage
772-
"|",
773-
("res_field", "=", False),
774-
("res_field", "!=", False),
782+
Domain.OR(
783+
[
784+
Domain("res_field", "=", False),
785+
Domain("res_field", "!=", False),
786+
]
787+
),
775788
]
776789
),
777-
normalize_domain(self._store_in_db_instead_of_object_storage_domain()),
790+
Domain(self._store_in_db_instead_of_object_storage_domain()),
778791
)
779792
)
780793

fs_attachment/models/ir_binary.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,24 @@ class IrBinary(models.AbstractModel):
1919
def _get_fs_attachment_for_field(self, record, field_name):
2020
if record._name == "ir.attachment" and record.fs_filename:
2121
return record
22-
23-
record.check_field_access_rights("read", [field_name])
24-
field_def = record._fields[field_name]
25-
if field_def.attachment and field_def.store:
26-
fs_attachment = (
27-
self.env["ir.attachment"]
28-
.sudo()
29-
.search(
30-
domain=[
31-
("res_model", "=", record._name),
32-
("res_id", "=", record.id),
33-
("res_field", "=", field_name),
34-
],
35-
limit=1,
22+
field_def = record._fields.get(field_name)
23+
if field_def:
24+
record._check_field_access(field_def, "read")
25+
if field_def.attachment and field_def.store:
26+
fs_attachment = (
27+
self.env["ir.attachment"]
28+
.sudo()
29+
.search(
30+
domain=[
31+
("res_model", "=", record._name),
32+
("res_id", "=", record.id),
33+
("res_field", "=", field_name),
34+
],
35+
limit=1,
36+
)
3637
)
37-
)
38-
if fs_attachment and fs_attachment.fs_filename:
39-
return fs_attachment
38+
if fs_attachment and fs_attachment.fs_filename:
39+
return fs_attachment
4040
return None
4141

4242
def _record_to_stream(self, record, field_name):

fs_attachment/readme/CONTRIBUTORS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
* Thierry Ducrest \<<thierry.ducrest@camptocamp.com>\>
1+
* Thierry Ducrest \<<thierry.ducrest@camptocamp.com>\>
22
* Guewen Baconnier \<<guewen.baconnier@camptocamp.com>\>
33
* Julien Coux \<<julien.coux@camptocamp.com>\>
44
* Akim Juillerat \<<akim.juillerat@camptocamp.com>\>

0 commit comments

Comments
 (0)