mirror of
https://github.com/bringout/oca-storage.git
synced 2026-04-19 17:32:02 +02:00
Initial commit: OCA Storage packages (17 packages)
This commit is contained in:
commit
7a380f05d3
659 changed files with 41828 additions and 0 deletions
228
odoo-bringout-oca-storage-fs_image/fs_image/fields.py
Normal file
228
odoo-bringout-oca-storage-fs_image/fs_image/fields.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
# Copyright 2023 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
# pylint: disable=method-required-super
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO, IOBase
|
||||
|
||||
from odoo import _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.image import image_process
|
||||
|
||||
from odoo.addons.fs_attachment.models.ir_attachment import IrAttachment
|
||||
from odoo.addons.fs_file.fields import FSFile, FSFileValue
|
||||
|
||||
|
||||
class FSImageValue(FSFileValue):
|
||||
"""Value for the FSImage field"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attachment: IrAttachment = None,
|
||||
name: str = None,
|
||||
value: bytes | IOBase = None,
|
||||
alt_text: str = None,
|
||||
) -> None:
|
||||
super().__init__(attachment, name, value)
|
||||
if self._attachment and alt_text is not None:
|
||||
raise ValueError(
|
||||
"FSImageValue cannot be initialized with an attachment and an"
|
||||
" alt_text at the same time. When initializing with an attachment,"
|
||||
" you can't pass any other argument."
|
||||
)
|
||||
self._alt_text = alt_text
|
||||
|
||||
@property
|
||||
def alt_text(self) -> str:
|
||||
alt_text = self._attachment.alt_text if self._attachment else self._alt_text
|
||||
return alt_text
|
||||
|
||||
@alt_text.setter
|
||||
def alt_text(self, value: str) -> None:
|
||||
if self._attachment:
|
||||
self._attachment.alt_text = value
|
||||
else:
|
||||
self._alt_text = value
|
||||
|
||||
@classmethod
|
||||
def from_fs_file_value(cls, fs_file_value: FSFileValue) -> "FSImageValue":
|
||||
if isinstance(fs_file_value, FSImageValue):
|
||||
return fs_file_value
|
||||
return cls(
|
||||
attachment=fs_file_value.attachment,
|
||||
name=fs_file_value.name if not fs_file_value.attachment else None,
|
||||
value=fs_file_value._buffer
|
||||
if not fs_file_value.attachment
|
||||
else fs_file_value._buffer,
|
||||
)
|
||||
|
||||
def image_process(
|
||||
self,
|
||||
size=(0, 0),
|
||||
verify_resolution=False,
|
||||
quality=0,
|
||||
crop=None,
|
||||
colorize=False,
|
||||
output_format="",
|
||||
):
|
||||
"""
|
||||
Process the image to adapt it to the given parameters.
|
||||
:param size: a tuple (max_width, max_height) containing the maximum
|
||||
width and height of the processed image.
|
||||
If one of the value is 0, it will be calculated to keep the aspect
|
||||
ratio.
|
||||
If both values are 0, the image will not be resized.
|
||||
:param verify_resolution: if True, make sure the original image size is not
|
||||
excessive before starting to process it. The max allowed resolution is
|
||||
defined by `IMAGE_MAX_RESOLUTION` in :class:`odoo.tools.image.ImageProcess`.
|
||||
:param int quality: quality setting to apply. Default to 0.
|
||||
|
||||
- for JPEG: 1 is worse, 95 is best. Values above 95 should be
|
||||
avoided. Falsy values will fallback to 95, but only if the image
|
||||
was changed, otherwise the original image is returned.
|
||||
- for PNG: set falsy to prevent conversion to a WEB palette.
|
||||
- for other formats: no effect.
|
||||
:param crop: (True | 'top' | 'bottom'):
|
||||
* True, the image will be cropped to the given size.
|
||||
* 'top', the image will be cropped at the top to the given size.
|
||||
* 'bottom', the image will be cropped at the bottom to the given size.
|
||||
Otherwise, it will be resized to fit the given size.
|
||||
:param colorize: if True, the transparent background of the image
|
||||
will be colorized in a random color.
|
||||
:param str output_format: the output format. Can be PNG, JPEG, GIF, or ICO.
|
||||
Default to the format of the original image. BMP is converted to
|
||||
PNG, other formats than those mentioned above are converted to JPEG.
|
||||
:return: the processed image as bytes
|
||||
"""
|
||||
return image_process(
|
||||
self.getvalue(),
|
||||
size=size,
|
||||
crop=crop,
|
||||
quality=quality,
|
||||
verify_resolution=verify_resolution,
|
||||
colorize=colorize,
|
||||
output_format=output_format,
|
||||
)
|
||||
|
||||
|
||||
class FSImage(FSFile):
|
||||
"""
|
||||
This field is a FSFile field with an alt_text attribute used to encapsulate
|
||||
an image file stored in a filesystem storage.
|
||||
|
||||
It's inspired by the 'image' field of odoo :class:`odoo.fields.Binary` but
|
||||
is designed to store the image in a filesystem storage instead of the
|
||||
database.
|
||||
|
||||
If image size is greater than the ``max_width``/``max_height`` limit of pixels,
|
||||
the image will be resized to the limit by keeping aspect ratio.
|
||||
|
||||
:param int max_width: the maximum width of the image (default: ``0``, no limit)
|
||||
:param int max_height: the maximum height of the image (default: ``0``, no limit)
|
||||
:param bool verify_resolution: whether the image resolution should be verified
|
||||
to ensure it doesn't go over the maximum image resolution
|
||||
(default: ``True``).
|
||||
See :class:`odoo.tools.image.ImageProcess` for maximum image resolution
|
||||
(default: ``50e6``).
|
||||
"""
|
||||
|
||||
type = "fs_image"
|
||||
|
||||
max_width = 0
|
||||
max_height = 0
|
||||
verify_resolution = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._image_process_mode = False
|
||||
|
||||
def create(self, record_values):
|
||||
with self._set_image_process_mode():
|
||||
return super().create(record_values)
|
||||
|
||||
def write(self, records, value):
|
||||
if isinstance(value, dict) and "content" not in value:
|
||||
# we are writing on the alt_text field only
|
||||
return self._update_alt_text(records, value)
|
||||
with self._set_image_process_mode():
|
||||
return super().write(records, value)
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
if not value:
|
||||
return None
|
||||
if isinstance(value, FSImageValue):
|
||||
cache_value = value
|
||||
else:
|
||||
cache_value = super().convert_to_cache(value, record, validate)
|
||||
if not isinstance(cache_value, FSImageValue):
|
||||
cache_value = FSImageValue.from_fs_file_value(cache_value)
|
||||
if isinstance(value, dict) and "alt_text" in value:
|
||||
cache_value.alt_text = value["alt_text"]
|
||||
if self._image_process_mode and cache_value.is_new:
|
||||
name = cache_value.name
|
||||
new_value = BytesIO(self._image_process(cache_value))
|
||||
cache_value._buffer = new_value
|
||||
cache_value.name = name
|
||||
return cache_value
|
||||
|
||||
def _create_attachment(self, record, cache_value):
|
||||
attachment = super()._create_attachment(record, cache_value)
|
||||
# odoo filter out additional fields in create method on ir.attachment
|
||||
# so we need to write the alt_text after the creation
|
||||
if cache_value.alt_text:
|
||||
attachment.alt_text = cache_value.alt_text
|
||||
return attachment
|
||||
|
||||
def _convert_attachment_to_cache(self, attachment: IrAttachment) -> FSImageValue:
|
||||
cache_value = super()._convert_attachment_to_cache(attachment)
|
||||
return FSImageValue.from_fs_file_value(cache_value)
|
||||
|
||||
def _image_process(self, cache_value: FSImageValue) -> bytes | None:
|
||||
if self.readonly and not self.max_width and not self.max_height:
|
||||
# no need to process images for computed fields, or related fields
|
||||
return cache_value.getvalue()
|
||||
return (
|
||||
cache_value.image_process(
|
||||
size=(self.max_width, self.max_height),
|
||||
verify_resolution=self.verify_resolution,
|
||||
)
|
||||
or None
|
||||
)
|
||||
|
||||
def convert_to_read(self, value, record, use_name_get=True) -> dict | None:
|
||||
vals = super().convert_to_read(value, record, use_name_get)
|
||||
if isinstance(value, FSImageValue):
|
||||
vals["alt_text"] = value.alt_text or None
|
||||
return vals
|
||||
|
||||
@contextmanager
|
||||
def _set_image_process_mode(self):
|
||||
self._image_process_mode = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._image_process_mode = False
|
||||
|
||||
def _process_related(self, value: FSImageValue):
|
||||
"""Override to resize the related value before saving it on self."""
|
||||
if not value:
|
||||
return None
|
||||
if self.readonly and not self.max_width and not self.max_height:
|
||||
# no need to process images for computed fields, or related fields
|
||||
# without max_width/max_height
|
||||
return value
|
||||
value = super()._process_related(value)
|
||||
new_value = BytesIO(self._image_process(value))
|
||||
return FSImageValue(value=new_value, alt_text=value.alt_text, name=value.name)
|
||||
|
||||
def _update_alt_text(self, records, value: dict):
|
||||
for record in records:
|
||||
if not record[self.name]:
|
||||
raise UserError(
|
||||
_(
|
||||
"Cannot set alt_text on empty image (record %(record)s.%(field_name)s)",
|
||||
record=record,
|
||||
field_name=self.name,
|
||||
)
|
||||
)
|
||||
record[self.name].alt_text = value["alt_text"]
|
||||
return True
|
||||
Loading…
Add table
Add a link
Reference in a new issue