mirror of
https://github.com/camptocamp/odoo-cloud-platform.git
synced 2026-06-23 18:04:34 +00:00
Merge pull request #49 from grindtildeath/12.0_add_base_fileurl_field
[12.0] Add base_fileurl_field
This commit is contained in:
+4
-2
@@ -18,8 +18,10 @@ addons:
|
||||
env:
|
||||
matrix:
|
||||
- LINT_CHECK="1"
|
||||
- TESTS="1" ODOO_REPO="odoo/odoo"
|
||||
- TESTS="1" ODOO_REPO="OCA/OCB"
|
||||
- TESTS="1" ODOO_REPO="odoo/odoo" EXCLUDE=test_base_fileurl_field
|
||||
- TESTS="1" ODOO_REPO="OCA/OCB" EXCLUDE=test_base_fileurl_field
|
||||
- TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE=test_base_fileurl_field
|
||||
- TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE=test_base_fileurl_field
|
||||
global:
|
||||
- VERSION="12.0" LINT_CHECK="0" TESTS="0"
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
Base FileURL Field
|
||||
==================
|
||||
|
||||
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 of 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 functionality of
|
||||
base_attachment_object_storage.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
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.
|
||||
|
||||
Limitations / Issues
|
||||
--------------------
|
||||
|
||||
* Filename must be stored in a separate field on the same model defining a FileURL field.
|
||||
* While using storage_path and filename attributes, there's a risk existing storage object
|
||||
are overwritten if files with the same filename are added on different records.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
cf `test_base_fileurl_field` module in https://github.com/camptocamp/odoo-cloud-platform
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import fields
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2012-2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
{
|
||||
"name": "Base FileURL Field",
|
||||
"summary": "Implementation of FileURL type fields",
|
||||
"version": "12.0.1.0.0",
|
||||
"category": "Technical Settings",
|
||||
'author': 'Camptocamp, Odoo Community Association (OCA)',
|
||||
'license': 'AGPL-3',
|
||||
"depends": [
|
||||
"base_attachment_object_storage",
|
||||
],
|
||||
"auto_install": False,
|
||||
"installable": True,
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# 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(fname)
|
||||
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(fname)
|
||||
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
|
||||
@@ -0,0 +1,4 @@
|
||||
Test Base FileURL Field
|
||||
=======================
|
||||
|
||||
This module serves as implementation example for `base_fileurl_field` and to run its tests.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
from . import models
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
{
|
||||
'name': 'test base fileurl fields',
|
||||
'summary': """A module to verify fileurl field.""",
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Tests',
|
||||
'author': 'Camptocamp,Odoo Community Association (OCA)',
|
||||
'license': 'AGPL-3',
|
||||
'depends': [
|
||||
'base_fileurl_field'
|
||||
],
|
||||
'data': [
|
||||
"views/res_partner.xml",
|
||||
"views/res_users.xml",
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1 @@
|
||||
This is a simple text file.
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
@@ -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)
|
||||
))
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
|
||||
_inherit = 'res.users'
|
||||
|
||||
partner_url_file = fields.FileURL(related='partner_id.url_file')
|
||||
partner_url_file_fname = fields.Char(related='partner_id.url_file_fname')
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import ir_attachment
|
||||
from . import test_fileurl_fields
|
||||
@@ -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 models, api
|
||||
|
||||
_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)
|
||||
@@ -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 i:
|
||||
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',
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_partner_form_inherit" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.inherit</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page name="fileurl_test" string="FileURL Test fields">
|
||||
<group string="Default widget">
|
||||
<field name="url_file" filename="url_file_fname" />
|
||||
<field name="url_file_fname" invisible="1"/>
|
||||
</group>
|
||||
<group string="Image widget">
|
||||
<field name="url_image" widget="image" filename="url_image_fname" />
|
||||
<field name="url_image_fname" invisible="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_users_form_inherit" model="ir.ui.view">
|
||||
<field name="name">res.users.form.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page name="fileurl_test" string="FileURL Test fields">
|
||||
<group string="Default widget">
|
||||
<field name="url_file" filename="url_file_fname" />
|
||||
<field name="url_file_fname" invisible="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user