From d83aca68e6dcffa9b6714bdde269722a417af356 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Fri, 8 Mar 2019 17:30:06 +0100 Subject: [PATCH] [12.0] Add base_fileurl_field --- base_fileurl_field/__init__.py | 2 + base_fileurl_field/__manifest__.py | 21 ++++ base_fileurl_field/fields.py | 102 ++++++++++++++++++ test_base_fileurl_field/__init__.py | 3 + test_base_fileurl_field/__manifest__.py | 16 +++ test_base_fileurl_field/data/pattern.png | Bin 0 -> 22849 bytes test_base_fileurl_field/data/sample.txt | 1 + test_base_fileurl_field/models/__init__.py | 1 + test_base_fileurl_field/models/res_partner.py | 44 ++++++++ test_base_fileurl_field/tests/__init__.py | 2 + .../tests/ir_attachment.py | 44 ++++++++ .../tests/test_fileurl_fields.py | 39 +++++++ test_base_fileurl_field/views/res_partner.xml | 22 ++++ 13 files changed, 297 insertions(+) create mode 100644 base_fileurl_field/__init__.py create mode 100644 base_fileurl_field/__manifest__.py create mode 100644 base_fileurl_field/fields.py create mode 100644 test_base_fileurl_field/__init__.py create mode 100644 test_base_fileurl_field/__manifest__.py create mode 100644 test_base_fileurl_field/data/pattern.png create mode 100644 test_base_fileurl_field/data/sample.txt create mode 100644 test_base_fileurl_field/models/__init__.py create mode 100644 test_base_fileurl_field/models/res_partner.py create mode 100644 test_base_fileurl_field/tests/__init__.py create mode 100644 test_base_fileurl_field/tests/ir_attachment.py create mode 100644 test_base_fileurl_field/tests/test_fileurl_fields.py create mode 100644 test_base_fileurl_field/views/res_partner.xml diff --git a/base_fileurl_field/__init__.py b/base_fileurl_field/__init__.py new file mode 100644 index 0000000..08405c5 --- /dev/null +++ b/base_fileurl_field/__init__.py @@ -0,0 +1,2 @@ +from . import fields + diff --git a/base_fileurl_field/__manifest__.py b/base_fileurl_field/__manifest__.py new file mode 100644 index 0000000..2f1f48e --- /dev/null +++ b/base_fileurl_field/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2012-2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Base FileURL Field", + "summary": "Implements of FileURL type fields", + "category": "Technical Settings", + "description": """ + This module adds a new field type FileURL to odoo. + FileURL is an extension of field type Binary, with the aim to store its + value on any kind external storage. + It's been built with the focus on Amazon S3 but could be used with + other storage solution as long as it extends the functionaly of + base_attachment_object_storage. + """, + "version": "12.0.1.0.0", + "depends": [ + "base_attachment_object_storage", + ], + "auto_install": False, + "installable": True, +} diff --git a/base_fileurl_field/fields.py b/base_fileurl_field/fields.py new file mode 100644 index 0000000..6c68288 --- /dev/null +++ b/base_fileurl_field/fields.py @@ -0,0 +1,102 @@ +# Copyright 2012-2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import unicodedata + +from odoo import fields + + +fields.Field.__doc__ += """ + + .. _field-fileurl: + + .. rubric:: FileURL fields + + FileURL fields is intended to store Binary data on an external storage + with the possibility to be accessed outside of odoo. + + :param storage_location: Required external storage that must be + activated on the system (cf base_attachment_storage) + + :param storage_path: Path to be used as a prefix to the filename in the + storage solution (must be used with filename) + + :param filename: Field on the same model which stores the filename. + Will be used to set fname on ir.attachment and, if storage_path is + defined, will be passed to force the storage key. +""" + + +class FileURL(fields.Binary): + + _slots = { + 'attachment': True, # Override default with True + 'storage_location': '', # External storage activated on the system (cf base_attachment_storage) # noqa + 'storage_path': '', # Path to be used as storage key (prefix of filename) # noqa + 'filename': '', # Field to use to store the filename on ir.attachment + } + + def create(self, record_values): + assert self.attachment + if not record_values: + return + # create the attachments that store the values + env = record_values[0][0].env + with env.norecompute(): + for record, value in record_values: + if not value: + continue + vals = { + 'name': self.name, + 'res_model': self.model_name, + 'res_field': self.name, + 'res_id': record.id, + 'type': 'binary', + 'datas': value, + } + fname = False + if self.filename: + fname = record[self.filename] + vals['datas_fname'] = fname + if fname and self.storage_path: + storage_key = self._build_storage_key( + record[self.filename] + ) + if not fname: + storage_key = False + env['ir.attachment'].sudo().with_context( + binary_field_real_user=env.user, + storage_location=self.storage_location, + force_storage_key=storage_key, + ).create(vals) + + def write(self, records, value): + for record in records: + storage_key = False + if self.filename: + fname = record[self.filename] + if fname and self.storage_path: + storage_key = self._build_storage_key( + record[self.filename]) + super().write( + records.with_context( + storage_location=self.storage_location, + force_storage_key=storage_key, + ), + value + ) + return True + + def _setup_regular_base(self, model): + super()._setup_regular_base(model) + if self.storage_path: + assert self.filename is not None, \ + "Field %s defines storage_path without filename" % self + + def _build_storage_key(self, filename): + return '/'.join([ + self.storage_path.rstrip('/'), + unicodedata.normalize('NFKC', filename) + ]) + + +fields.FileURL = FileURL diff --git a/test_base_fileurl_field/__init__.py b/test_base_fileurl_field/__init__.py new file mode 100644 index 0000000..7967689 --- /dev/null +++ b/test_base_fileurl_field/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from . import models diff --git a/test_base_fileurl_field/__manifest__.py b/test_base_fileurl_field/__manifest__.py new file mode 100644 index 0000000..498a2ce --- /dev/null +++ b/test_base_fileurl_field/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + 'name': 'test base fileurl fields', + 'version': '12.0.1.0.0', + 'category': 'Tests', + 'description': """A module to verify fileurl field.""", + 'depends': [ + 'base_fileurl_field' + ], + 'data': [ + "views/res_partner.xml", + ], + 'installable': True, + 'auto_install': False, +} diff --git a/test_base_fileurl_field/data/pattern.png b/test_base_fileurl_field/data/pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..ce52599f68d19cb2bbbb5639a453ac3a6cc876b6 GIT binary patch literal 22849 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 literal 0 HcmV?d00001 diff --git a/test_base_fileurl_field/data/sample.txt b/test_base_fileurl_field/data/sample.txt new file mode 100644 index 0000000..5251e0f --- /dev/null +++ b/test_base_fileurl_field/data/sample.txt @@ -0,0 +1 @@ +This is a simple text file. \ No newline at end of file diff --git a/test_base_fileurl_field/models/__init__.py b/test_base_fileurl_field/models/__init__.py new file mode 100644 index 0000000..91fed54 --- /dev/null +++ b/test_base_fileurl_field/models/__init__.py @@ -0,0 +1 @@ +from . import res_partner diff --git a/test_base_fileurl_field/models/res_partner.py b/test_base_fileurl_field/models/res_partner.py new file mode 100644 index 0000000..359843c --- /dev/null +++ b/test_base_fileurl_field/models/res_partner.py @@ -0,0 +1,44 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class ResPartner(models.Model): + + _inherit = 'res.partner' + + name = fields.Char() + url_file = fields.FileURL( + storage_location='s3', + filename='url_file_fname', + storage_path='partner' + ) + url_file_fname = fields.Char() + + url_image = fields.FileURL( + storage_location='s3', + filename='url_image_fname', + storage_path='partner_image', + ) + url_image_fname = fields.Char() + + @api.constrains('url_file', 'url_file_fname') + def _check_url_file_fname(self): + rec = self.search([('url_file_fname', '=', self.url_file_fname)]) + if len(rec) > 1: + raise ValidationError(_( + "This file name is already used on an existing record. " + "Please use another file name or delete the url_file on :\n" + "Model: %s Id: %s" % (self._name, rec.id) + )) + + @api.constrains('url_image', 'url_image_fname') + def _check_url_image_fname(self): + rec = self.search([('url_image_fname', '=', self.url_image_fname)]) + if len(rec) > 1: + raise ValidationError(_( + "This file name is already used on an existing record. " + "Please use another file name or delete the url_image on :\n" + "Model: %s Id: %s" % (self._name, rec.id) + )) diff --git a/test_base_fileurl_field/tests/__init__.py b/test_base_fileurl_field/tests/__init__.py new file mode 100644 index 0000000..56a55f4 --- /dev/null +++ b/test_base_fileurl_field/tests/__init__.py @@ -0,0 +1,2 @@ +from . import ir_attachment +from . import test_fileurl_fields diff --git a/test_base_fileurl_field/tests/ir_attachment.py b/test_base_fileurl_field/tests/ir_attachment.py new file mode 100644 index 0000000..8c5ec32 --- /dev/null +++ b/test_base_fileurl_field/tests/ir_attachment.py @@ -0,0 +1,44 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +import logging + +from odoo import _, api, exceptions, models + +_logger = logging.getLogger(__name__) + +FAKE_S3_BUCKET = {} + + +class IrAttachment(models.Model): + _inherit = "ir.attachment" + + def _get_stores(self): + l = ['s3'] + l += super(IrAttachment, self)._get_stores() + return l + + @api.model + def _store_file_read(self, fname, bin_size=False): + if fname.startswith('s3://'): + return FAKE_S3_BUCKET.get(fname) + else: + return super(IrAttachment, self)._store_file_read(fname, bin_size) + + @api.model + def _store_file_write(self, key, bin_data): + location = self.env.context.get('storage_location') or self._storage() + if location == 's3': + FAKE_S3_BUCKET[key] = bin_data + filename = 's3://fake_bucket/%s' % key + else: + _super = super(IrAttachment, self) + filename = _super._store_file_write(key, bin_data) + return filename + + @api.model + def _store_file_delete(self, fname): + if fname.startswith('s3://'): + FAKE_S3_BUCKET.pop(fname) + else: + super(IrAttachment, self)._store_file_delete(fname) diff --git a/test_base_fileurl_field/tests/test_fileurl_fields.py b/test_base_fileurl_field/tests/test_fileurl_fields.py new file mode 100644 index 0000000..e727715 --- /dev/null +++ b/test_base_fileurl_field/tests/test_fileurl_fields.py @@ -0,0 +1,39 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import base64 + +from odoo.tests import TransactionCase +from odoo.modules.module import get_module_resource +from odoo.exceptions import ValidationError + + +class TestFileUrlFields(TransactionCase): + + def test_fileurl_fields(self): + file_path = get_module_resource('test_base_fileurl_field', 'data', + 'sample.txt') + image_path = get_module_resource('test_base_fileurl_field', 'data', + 'pattern.png') + partner = self.env.ref('base.main_partner') + with open(file_path, 'rb') as f: + with open(image_path, 'rb') as i: + partner.write({ + 'url_file': base64.b64encode(f.read()), + 'url_file_fname': 'sample.txt', + 'url_image': base64.b64encode(i.read()), + 'url_image_fname': 'pattern.png', + }) + + with open(file_path, 'rb') as f: + self.assertEqual(base64.decodebytes(partner.url_file), f.read()) + + with open(image_path, 'rb') as image: + self.assertEqual(base64.decodebytes(partner.url_image), i.read()) + + partner2 = self.env.ref('base.partner_admin') + with open(file_path, 'rb') as f: + with self.assertRaises(ValidationError): + partner2.write({ + 'url_file': base64.b64encode(f.read()), + 'url_file_fname': 'sample.txt', + }) diff --git a/test_base_fileurl_field/views/res_partner.xml b/test_base_fileurl_field/views/res_partner.xml new file mode 100644 index 0000000..08c33e4 --- /dev/null +++ b/test_base_fileurl_field/views/res_partner.xml @@ -0,0 +1,22 @@ + + + + res.partner.form.inherit + res.partner + + + + + + + + + + + + + + + + +