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 0000000..ce52599 Binary files /dev/null and b/test_base_fileurl_field/data/pattern.png differ 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 + + + + + + + + + + + + + + + + +