Skip to content

Commit b307c79

Browse files
committed
🎉 ir_attachment_google_drive
1 parent 15b7e93 commit b307c79

10 files changed

Lines changed: 364 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
.. image:: https://img.shields.io/badge/license-MIT-blue.svg
2+
:target: https://opensource.org/licenses/MIT
3+
:alt: License: MIT
4+
5+
=================================
6+
Google Drive Attachment Storage
7+
=================================
8+
9+
TODO description intro
10+
11+
TODO detailed description
12+
13+
Credits
14+
=======
15+
16+
Contributors
17+
------------
18+
* `Eugene Molotov <https://it-projects.info/team/em230418>`__:
19+
20+
* :one::zero: init version of the module
21+
22+
Sponsors
23+
--------
24+
* `IT-Projects LLC <https://it-projects.info>`__
25+
26+
Maintainers
27+
-----------
28+
* `IT-Projects LLC <https://it-projects.info>`__
29+
30+
To get a guaranteed support
31+
you are kindly requested to purchase the module
32+
at `odoo apps store <https://apps.odoo.com/apps/modules/11.0/ir_attachment_google_drive/>`__.
33+
34+
Thank you for understanding!
35+
36+
`IT-Projects Team <https://www.it-projects.info/team>`__
37+
38+
Further information
39+
===================
40+
41+
Demo: http://runbot.it-projects.info/demo/misc-addons/11.0
42+
43+
HTML Description: https://apps.odoo.com/apps/modules/11.0/ir_attachment_google_drive/
44+
45+
Usage instructions: `<doc/index.rst>`_
46+
47+
Changelog: `<doc/changelog.rst>`_
48+
49+
Notifications on updates: `via Atom <https://github.com/it-projects-llc/misc-addons/commits/11.0/ir_attachment_google_drive.atom>`_, `by Email <https://blogtrottr.com/?subscribe=https://github.com/it-projects-llc/misc-addons/commits/11.0/ir_attachment_google_drive.atom>`_
50+
51+
Tested on Odoo 11.0 a827d3015c6994bc3c779f9ba5cd270d8bdd8edd
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# License MIT (https://opensource.org/licenses/MIT).
2+
3+
from . import models
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2020 Eugene Molotov <https://it-projects.info/team/em230418>
2+
# License MIT (https://opensource.org/licenses/MIT).
3+
4+
{
5+
"name": """Google Drive Attachment Storage""",
6+
"summary": """TODO description intro""",
7+
"category": "Extra Tools",
8+
# "live_test_url": "http://apps.it-projects.info/shop/product/DEMO-URL?version=12.0",
9+
"images": [],
10+
"version": "11.0.1.0.0",
11+
"application": False,
12+
"author": "IT-Projects LLC, Eugene Molotov",
13+
"support": "apps@it-projects.info",
14+
"website": "https://apps.odoo.com/apps/modules/11.0/ir_attachment_google_drive/",
15+
"license": "Other OSI approved licence", # MIT
16+
# "price": 9.00,
17+
# "currency": "EUR",
18+
"depends": ["base", "google_drive"],
19+
"external_dependencies": {"python": [], "bin": []},
20+
"data": ["views/res_config_settings_views.xml"],
21+
"demo": [],
22+
"qweb": [],
23+
"post_load": None,
24+
"pre_init_hook": None,
25+
"post_init_hook": None,
26+
"uninstall_hook": None,
27+
"auto_install": False,
28+
"installable": True,
29+
# "demo_title": "Google Drive Attachment Storage",
30+
# "demo_addons": [
31+
# ],
32+
# "demo_addons_hidden": [
33+
# ],
34+
# "demo_url": "DEMO-URL",
35+
# "demo_summary": "TODO description intro",
36+
# "demo_images": [
37+
# "images/MAIN_IMAGE",
38+
# ]
39+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
`1.0.0`
2+
-------
3+
4+
- **Init version**
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
=================================
2+
Google Drive Attachment Storage
3+
=================================
4+
5+
Installation
6+
============
7+
{Instruction about things to do before actual installation}
8+
9+
* {OPTIONAL }`Activate longpolling <https://odoo-development.readthedocs.io/en/latest/admin/longpolling.html>`__
10+
* {Additional notes if any}
11+
* `Install <https://odoo-development.readthedocs.io/en/latest/odoo/usage/install-module.html>`__ this module in a usual way
12+
13+
Configuration
14+
=============
15+
16+
{Instruction how to configure the module before start to use it}
17+
18+
* `Log in as SUPERUSER <https://odoo-development.readthedocs.io/en/latest/odoo/usage/login-as-superuser.html>`__
19+
* `Activate Developer Mode <https://odoo-development.readthedocs.io/en/latest/odoo/usage/debug-mode.html>`__
20+
* Open menu ``[[ {Menu} ]] >> {Submenu} >> {Subsubmenu}``
21+
* Click ``[{Button Name}]``
22+
23+
Usage
24+
=====
25+
26+
{Instruction for daily usage. It should describe how to check that module works. What shall user do and what would user get.}
27+
28+
* Open menu ``[[ {Menu} ]]>> {Submenu} >> {Subsubmenu}``
29+
* Click ``[{Button Name}]``
30+
* RESULT: {what user gets, how the modules changes default behaviour}
31+
32+
Uninstallation
33+
==============
34+
35+
{Optional section for uninstallation notes. Delete it if you don't have notes for uninstallation.}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
2+
3+
from . import ir_attachment
4+
from . import res_config_settings
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Copyright 2020 Eugene Molotov <https://it-projects.info/team/em230418>
2+
# License MIT (https://opensource.org/licenses/MIT).
3+
4+
import base64
5+
import json
6+
import logging
7+
8+
import requests
9+
from urllib3.fields import RequestField
10+
from urllib3.filepost import choose_boundary, encode_multipart_formdata
11+
12+
from odoo import api, models
13+
from odoo.tools import human_size
14+
from odoo.tools.safe_eval import safe_eval
15+
16+
from odoo.addons.google_account.models.google_service import TIMEOUT
17+
18+
_logger = logging.getLogger(__name__)
19+
_logger.setLevel(logging.DEBUG)
20+
21+
PREFIX = "google_drive://"
22+
23+
24+
# https://stackoverflow.com/a/47682897
25+
def encode_multipart_related(fields, boundary=None):
26+
if boundary is None:
27+
boundary = choose_boundary()
28+
29+
body, _ = encode_multipart_formdata(fields, boundary)
30+
content_type = str("multipart/related; boundary=%s" % boundary)
31+
32+
return body, content_type
33+
34+
35+
def encode_media_related(metadata, media, media_content_type):
36+
rf1 = RequestField(
37+
name="placeholder",
38+
data=json.dumps(metadata),
39+
headers={"Content-Type": "application/json; charset=UTF-8"},
40+
)
41+
rf2 = RequestField(
42+
name="placeholder2", data=media, headers={"Content-Type": media_content_type},
43+
)
44+
return encode_multipart_related([rf1, rf2])
45+
46+
47+
class IrAttachment(models.Model):
48+
49+
_inherit = "ir.attachment"
50+
51+
def _get_google_drive_auth_header(self):
52+
access_token = self.env.context.get("google_drive_access_token")
53+
if not access_token:
54+
access_token = self.env["google.drive.config"].get_access_token()
55+
return {"Authorization": "Bearer " + access_token}
56+
57+
# это нагло скопировано с ir_attachment_url
58+
@api.multi
59+
def _filter_protected_attachments(self):
60+
return self.filtered(
61+
lambda r: r.res_model not in ["ir.ui.view", "ir.ui.menu"]
62+
or not r.name.startswith("/web/content/")
63+
or not r.name.startswith("/web/static/")
64+
)
65+
66+
def _inverse_datas(self):
67+
condition = (
68+
self.env["ir.config_parameter"]
69+
.sudo()
70+
.get_param("google_drive.attachment_condition")
71+
)
72+
73+
if condition:
74+
condition = safe_eval(condition, mode="eval")
75+
our_records = self.sudo().search([("id", "in", self.ids)] + condition)
76+
else:
77+
our_records = self
78+
79+
our_records = our_records._filter_protected_attachments()
80+
81+
if our_records:
82+
# make sure, you can use google drive
83+
# otherwise - use default behaviour
84+
try:
85+
google_drive_access_token = self.env[
86+
"google.drive.config"
87+
].get_access_token()
88+
except Exception:
89+
_logger.exception(
90+
"Google Drive is not configured properly. Keeping attachments as usual"
91+
)
92+
return super(IrAttachment, self)._inverse_datas()
93+
94+
self = self.with_context(
95+
google_drive_access_token=google_drive_access_token
96+
)
97+
98+
for attach in our_records:
99+
bin_value = base64.b64decode(attach.datas)
100+
fname = self._file_write_google_drive(bin_value, attach.datas_fname)
101+
vals = {
102+
"file_size": len(bin_value),
103+
"checksum": self._compute_checksum(bin_value),
104+
"index_content": self._index(
105+
bin_value, attach.datas_fname, attach.mimetype
106+
),
107+
"store_fname": fname,
108+
"db_datas": False,
109+
"type": "binary",
110+
"datas_fname": attach.datas_fname,
111+
}
112+
super(IrAttachment, attach.sudo()).write(vals)
113+
114+
return super(IrAttachment, self - our_records)._inverse_datas()
115+
116+
def _file_read(self, fname, bin_size=False):
117+
if not fname.startswith(PREFIX):
118+
return super(IrAttachment, self)._file_read(fname, bin_size)
119+
120+
file_id = fname[len(PREFIX) :]
121+
_logger.debug("reading file with id {}".format(file_id))
122+
123+
if bin_size:
124+
request_url = "https://www.googleapis.com/drive/v2/files/{}".format(file_id)
125+
else:
126+
request_url = "https://www.googleapis.com/drive/v2/files/{}?alt=media".format(
127+
file_id
128+
)
129+
130+
r = requests.get(
131+
request_url, headers=self._get_google_drive_auth_header(), timeout=TIMEOUT
132+
)
133+
r.raise_for_status()
134+
135+
if bin_size:
136+
return human_size(int(r.json()["fileSize"]))
137+
else:
138+
return base64.b64encode(r.content)
139+
140+
def _file_write_google_drive(self, bin_value, original_filename):
141+
metadata = {}
142+
if original_filename:
143+
metadata["title"] = original_filename
144+
145+
data, content_type = encode_media_related(
146+
metadata, bin_value, "application/octet-stream"
147+
)
148+
149+
headers = self._get_google_drive_auth_header()
150+
headers["Content-Type"] = content_type
151+
152+
r = requests.post(
153+
"https://www.googleapis.com/upload/drive/v2/files?uploadType=multipart",
154+
headers=headers,
155+
data=data,
156+
)
157+
r.raise_for_status()
158+
file_id = r.json()["id"]
159+
_logger.debug("uploaded file with id {}".format(file_id))
160+
return PREFIX + file_id
161+
162+
def _file_delete(self, fname):
163+
if not fname.startswith(PREFIX):
164+
return super(IrAttachment, self)._file_delete(fname)
165+
166+
file_id = fname[len(PREFIX) :]
167+
168+
r = requests.delete(
169+
"https://www.googleapis.com/drive/v2/files/{}".format(file_id),
170+
headers=self._get_google_drive_auth_header(),
171+
timeout=TIMEOUT,
172+
)
173+
r.raise_for_status()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2020 Eugene Molotov <https://it-projects.info/team/em230418>
2+
# License MIT (https://opensource.org/licenses/MIT).
3+
4+
from odoo import api, fields, models
5+
6+
7+
class ResConfigSettings(models.TransientModel):
8+
9+
_inherit = "res.config.settings"
10+
11+
google_drive_attachment_condition = fields.Char(
12+
string="Google Drive Condition",
13+
help="""Specify valid odoo search domain here,
14+
e.g. [('res_model', 'in', ['product.image'])] -- store data of product.image only.
15+
Empty condition means all models""",
16+
config_parameter="google_drive.attachment_condition",
17+
)
18+
19+
@api.model
20+
def get_values(self):
21+
res = super(ResConfigSettings, self).get_values()
22+
icp = self.env["ir.config_parameter"].sudo()
23+
24+
res.update(
25+
google_drive_attachment_condition=icp.get_param(
26+
"google_drive.attachment_condition", ""
27+
),
28+
)
29+
return res
30+
31+
def set_values(self):
32+
super(ResConfigSettings, self).set_values()
33+
icp = self.env["ir.config_parameter"].sudo()
34+
icp.set_param(
35+
"google_drive.attachment_condition",
36+
self.google_drive_attachment_condition or "",
37+
)
2.96 KB
Loading
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!-- Copyright 2020 Eugene Molotov <https://it-projects.info/team/em230418>
3+
License MIT (https://opensource.org/licenses/MIT). -->
4+
<odoo>
5+
<record id="res_config_settings_view_form" model="ir.ui.view">
6+
<field name="name">res.config.settings.view.form.inherit.google.drive</field>
7+
<field name="model">res.config.settings</field>
8+
<field name="inherit_id" ref="google_drive.res_config_settings_view_form"/>
9+
<field name="arch" type="xml">
10+
<xpath expr="//field[@name='google_drive_uri']/../../.." position="inside">
11+
<div class="mt16 row">
12+
<label for="google_drive_attachment_condition" class="col-md-3 o_light_label"/>
13+
<field name="google_drive_attachment_condition"/>
14+
</div>
15+
</xpath>
16+
</field>
17+
</record>
18+
</odoo>

0 commit comments

Comments
 (0)