From 17fe51ee796b64e8712ef6cc7ecfd27adfb1fee4 Mon Sep 17 00:00:00 2001 From: Eugene Molotov Date: Mon, 16 Mar 2020 17:32:38 +0500 Subject: [PATCH] :tada: ir_attachment_google_cloud_storage --- ir_attachment_google_cloud_storage/README.rst | 51 +++++++++ .../__init__.py | 3 + .../__manifest__.py | 39 +++++++ .../doc/changelog.rst | 4 + .../doc/index.rst | 35 ++++++ .../models/__init__.py | 4 + .../models/ir_attachment.py | 104 ++++++++++++++++++ .../models/res_config_settings.py | 74 +++++++++++++ .../static/description/icon.png | Bin 0 -> 3035 bytes .../views/res_config_settings_views.xml | 30 +++++ 10 files changed, 344 insertions(+) create mode 100644 ir_attachment_google_cloud_storage/README.rst create mode 100644 ir_attachment_google_cloud_storage/__init__.py create mode 100644 ir_attachment_google_cloud_storage/__manifest__.py create mode 100644 ir_attachment_google_cloud_storage/doc/changelog.rst create mode 100644 ir_attachment_google_cloud_storage/doc/index.rst create mode 100644 ir_attachment_google_cloud_storage/models/__init__.py create mode 100644 ir_attachment_google_cloud_storage/models/ir_attachment.py create mode 100644 ir_attachment_google_cloud_storage/models/res_config_settings.py create mode 100644 ir_attachment_google_cloud_storage/static/description/icon.png create mode 100644 ir_attachment_google_cloud_storage/views/res_config_settings_views.xml diff --git a/ir_attachment_google_cloud_storage/README.rst b/ir_attachment_google_cloud_storage/README.rst new file mode 100644 index 000000000..2b9fad64a --- /dev/null +++ b/ir_attachment_google_cloud_storage/README.rst @@ -0,0 +1,51 @@ +.. image:: https://img.shields.io/badge/license-MIT-blue.svg + :target: https://opensource.org/licenses/MIT + :alt: License: MIT + +================================= + Google Cloud Storage Attachment Storage +================================= + +TODO description intro + +TODO detailed description + +Credits +======= + +Contributors +------------ +* `Eugene Molotov `__: + + * :one::zero: init version of the module + +Sponsors +-------- +* `IT-Projects LLC `__ + +Maintainers +----------- +* `IT-Projects LLC `__ + + To get a guaranteed support + you are kindly requested to purchase the module + at `odoo apps store `__. + + Thank you for understanding! + + `IT-Projects Team `__ + +Further information +=================== + +Demo: http://runbot.it-projects.info/demo/misc-addons/11.0 + +HTML Description: https://apps.odoo.com/apps/modules/11.0/ir_attachment_google_cloud_storage/ + +Usage instructions: ``_ + +Changelog: ``_ + +Notifications on updates: `via Atom `_, `by Email `_ + +Tested on Odoo 11.0 a827d3015c6994bc3c779f9ba5cd270d8bdd8edd diff --git a/ir_attachment_google_cloud_storage/__init__.py b/ir_attachment_google_cloud_storage/__init__.py new file mode 100644 index 000000000..1d99e04b6 --- /dev/null +++ b/ir_attachment_google_cloud_storage/__init__.py @@ -0,0 +1,3 @@ +# License MIT (https://opensource.org/licenses/MIT). + +from . import models diff --git a/ir_attachment_google_cloud_storage/__manifest__.py b/ir_attachment_google_cloud_storage/__manifest__.py new file mode 100644 index 000000000..abf2b199b --- /dev/null +++ b/ir_attachment_google_cloud_storage/__manifest__.py @@ -0,0 +1,39 @@ +# Copyright 2020 Eugene Molotov +# License MIT (https://opensource.org/licenses/MIT). + +{ + "name": """Google Cloud Storage Attachment Storage""", + "summary": """TODO description intro""", + "category": "Extra Tools", + # "live_test_url": "http://apps.it-projects.info/shop/product/DEMO-URL?version=12.0", + "images": [], + "version": "13.0.1.0.0", + "application": False, + "author": "IT-Projects LLC, Eugene Molotov", + "support": "apps@it-projects.info", + "website": "https://apps.odoo.com/apps/modules/11.0/ir_attachment_google_storage/", + "license": "Other OSI approved licence", # MIT + # "price": 9.00, + # "currency": "EUR", + "depends": ["base", "base_setup"], + "external_dependencies": {"python": ["google-cloud-storage"], "bin": []}, + "data": ["views/res_config_settings_views.xml"], + "demo": [], + "qweb": [], + "post_load": None, + "pre_init_hook": None, + "post_init_hook": None, + "uninstall_hook": None, + "auto_install": False, + "installable": True, + # "demo_title": "Google Drive Attachment Storage", + # "demo_addons": [ + # ], + # "demo_addons_hidden": [ + # ], + # "demo_url": "DEMO-URL", + # "demo_summary": "TODO description intro", + # "demo_images": [ + # "images/MAIN_IMAGE", + # ] +} diff --git a/ir_attachment_google_cloud_storage/doc/changelog.rst b/ir_attachment_google_cloud_storage/doc/changelog.rst new file mode 100644 index 000000000..5583eb326 --- /dev/null +++ b/ir_attachment_google_cloud_storage/doc/changelog.rst @@ -0,0 +1,4 @@ +`1.0.0` +------- + +- **Init version** diff --git a/ir_attachment_google_cloud_storage/doc/index.rst b/ir_attachment_google_cloud_storage/doc/index.rst new file mode 100644 index 000000000..d3870a75f --- /dev/null +++ b/ir_attachment_google_cloud_storage/doc/index.rst @@ -0,0 +1,35 @@ +================================= + Google Drive Attachment Storage +================================= + +Installation +============ +{Instruction about things to do before actual installation} + +* {OPTIONAL }`Activate longpolling `__ +* {Additional notes if any} +* `Install `__ this module in a usual way + +Configuration +============= + +{Instruction how to configure the module before start to use it} + +* `Log in as SUPERUSER `__ +* `Activate Developer Mode `__ +* Open menu ``[[ {Menu} ]] >> {Submenu} >> {Subsubmenu}`` +* Click ``[{Button Name}]`` + +Usage +===== + +{Instruction for daily usage. It should describe how to check that module works. What shall user do and what would user get.} + +* Open menu ``[[ {Menu} ]]>> {Submenu} >> {Subsubmenu}`` +* Click ``[{Button Name}]`` +* RESULT: {what user gets, how the modules changes default behaviour} + +Uninstallation +============== + +{Optional section for uninstallation notes. Delete it if you don't have notes for uninstallation.} diff --git a/ir_attachment_google_cloud_storage/models/__init__.py b/ir_attachment_google_cloud_storage/models/__init__.py new file mode 100644 index 000000000..fcf9596c4 --- /dev/null +++ b/ir_attachment_google_cloud_storage/models/__init__.py @@ -0,0 +1,4 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from . import ir_attachment +from . import res_config_settings diff --git a/ir_attachment_google_cloud_storage/models/ir_attachment.py b/ir_attachment_google_cloud_storage/models/ir_attachment.py new file mode 100644 index 000000000..64a0ff5a6 --- /dev/null +++ b/ir_attachment_google_cloud_storage/models/ir_attachment.py @@ -0,0 +1,104 @@ +# Copyright 2020 Eugene Molotov +# License MIT (https://opensource.org/licenses/MIT). + +import base64 +import json +import logging + +from odoo import api, models +from odoo.tools import human_size + +_logger = logging.getLogger(__name__) +_logger.setLevel(logging.DEBUG) + +PREFIX = "google_cloud_storage://" + + +class IrAttachment(models.Model): + + _inherit = "ir.attachment" + + @api.model_create_multi + def create(self, vals_list): + try: + bucket = self.env["res.config.settings"].get_google_cloud_storage_bucket() + except Exception: + _logger.exception( + "Google Cloud Storage is not configured properly. Keeping attachments as usual" + ) + return super(IrAttachment, self).create(vals_list) + + # based on https://github.com/odoo/odoo/blob/fa852ba1c5707b71469c410063f338eef261ab2b/odoo/addons/base/models/ir_attachment.py#L506-L524 + record_tuple_set = set() + for values in vals_list: + # remove computed field depending of datas + for field in ('file_size', 'checksum'): + values.pop(field, False) + values = self._check_contents(values) + if 'datas' in values: + # =============== + # check, if attachment must be saved as google drive attachment + # start + data = values.pop('datas') + mimetype = values.pop('mimetype') + if values.get("res_model") not in ["ir.ui.view", "ir.ui.menu"] and self._storage() != 'db' and data: + bin_data = base64.b64decode(data) if data else b'' + checksum = self._compute_checksum(bin_data) + values.update({ + 'file_size': len(bin_data), + 'checksum': checksum, + 'index_content': self._index(bin_data, mimetype), + 'store_fname': self._file_write_google_cloud_storage(bucket, bin_data, checksum), + 'db_datas': False, + }) + else: + values.update(self._get_datas_related_values(data, mimetype)) + # end + # =============== + # 'check()' only uses res_model and res_id from values, and make an exists. + # We can group the values by model, res_id to make only one query when + # creating multiple attachments on a single record. + record_tuple = (values.get('res_model'), values.get('res_id')) + record_tuple_set.add(record_tuple) + for record_tuple in record_tuple_set: + (res_model, res_id) = record_tuple + self.check('create', values={'res_model':res_model, 'res_id':res_id}) + return super(IrAttachment, self).create(vals_list) + + def _file_read(self, fname, bin_size=False): + if not fname.startswith(PREFIX): + return super(IrAttachment, self)._file_read(fname, bin_size) + + bucket = self.env["res.config.settings"].get_google_cloud_storage_bucket() + + file_id = fname[len(PREFIX) :] + _logger.debug("reading file with id {}".format(file_id)) + + blob = bucket.get_blob(file_id) + + if bin_size: + return human_size(blob.size) + else: + return base64.b64encode(blob.download_as_string()) + + def _file_write_google_cloud_storage(self, bucket, bin_value, checksum): + file_id = "odoo/{}".format(checksum) + + blob = bucket.blob(file_id) + blob.upload_from_string(bin_value) + + _logger.debug("uploaded file with id {}".format(file_id)) + return PREFIX + file_id + + def _file_delete(self, fname): + if not fname.startswith(PREFIX): + return super(IrAttachment, self)._file_delete(fname) + + bucket = self.env["res.config.settings"].get_google_cloud_storage_bucket() + + file_id = fname[len(PREFIX) :] + _logger.debug("deleting file with id {}".format(file_id)) + + blob = bucket.get_blob(file_id) + + blob.delete() diff --git a/ir_attachment_google_cloud_storage/models/res_config_settings.py b/ir_attachment_google_cloud_storage/models/res_config_settings.py new file mode 100644 index 000000000..619651e2b --- /dev/null +++ b/ir_attachment_google_cloud_storage/models/res_config_settings.py @@ -0,0 +1,74 @@ +# Copyright 2020 Eugene Molotov +# License MIT (https://opensource.org/licenses/MIT). + +from odoo import api, fields, models +from google.oauth2 import service_account +from google.cloud import storage +import json + +class ResConfigSettings(models.TransientModel): + + _inherit = "res.config.settings" + + google_cloud_storage_credentials = fields.Char( + string="Google Application Credentials (for Google Cloud Storage)", + config_parameter="google_cloud_storage.credentials" + ) + google_cloud_storage_bucket = fields.Char( + string="Bucket (for Google Cloud Storage)", + config_parameter="google_cloud_storage.bucket" + ) + + + def get_google_cloud_storage_client(self): + icp = self.env["ir.config_parameter"].sudo() + credentials = icp.get_param("google_cloud_storage.credentials") + + if not credentials: + raise Exception("No Google Cloud Storage credendtials given") + + return storage.Client( + None, + credentials=service_account.Credentials.from_service_account_info( + json.loads(credentials) + ) + ) + + def get_google_cloud_storage_bucket(self): + client = self.get_google_cloud_storage_client() + + icp = self.env["ir.config_parameter"].sudo() + bucket = icp.get_param("google_cloud_storage.bucket") + + if not bucket: + raise Exception("No Google Cloud Storage bucket given") + + return client.get_bucket(bucket) + + + @api.model + def get_values(self): + res = super(ResConfigSettings, self).get_values() + icp = self.env["ir.config_parameter"].sudo() + + res.update( + google_cloud_storage_credentials=icp.get_param( + "google_cloud_storage.credentials", "" + ), + google_cloud_storage_bucket=icp.get_param( + "google_cloud_storage.bucket", "" + ), + ) + return res + + def set_values(self): + super(ResConfigSettings, self).set_values() + icp = self.env["ir.config_parameter"].sudo() + icp.set_param( + "google_cloud_storage.credentials", + self.google_cloud_storage_credentials or "{}", + ) + icp.set_param( + "google_cloud_storage.bucket", + self.google_cloud_storage_bucket or "", + ) diff --git a/ir_attachment_google_cloud_storage/static/description/icon.png b/ir_attachment_google_cloud_storage/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b43a0a135f903a4c0c401f03a8690fbbd021a2ab GIT binary patch literal 3035 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 + + + + res.config.settings.view.form.inherit.base_setup + res.config.settings + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+