[IMP]document_url_google_drive: add module to select documents to attach directly from Google Drive

This commit is contained in:
Demchuk Mykola 2023-11-05 21:31:00 +02:00
parent acebd9ca3b
commit e5ce59e9b2
24 changed files with 751 additions and 0 deletions

View File

@ -0,0 +1,35 @@
**This file is going to be generated by oca-gen-addon-readme.**
*Manual changes will be overwritten.*
Please provide content in the ``readme`` directory:
* **DESCRIPTION.rst** (required)
* INSTALL.rst (optional)
* CONFIGURE.rst (optional)
* **USAGE.rst** (optional, highly recommended)
* DEVELOP.rst (optional)
* ROADMAP.rst (optional)
* HISTORY.rst (optional, recommended)
* **CONTRIBUTORS.rst** (optional, highly recommended)
* CREDITS.rst (optional)
Content of this README will also be drawn from the addon manifest,
from keys such as name, authors, maintainers, development_status,
and license.
A good, one sentence summary in the manifest is also highly recommended.
Automatic changelog generation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`HISTORY.rst` can be auto generated using `towncrier <https://pypi.org/project/towncrier>`_.
Just put towncrier compatible changelog fragments into `readme/newsfragments`
and the changelog file will be automatically generated and updated when a new fragment is added.
Please refer to `towncrier` documentation to know more.
NOTE: the changelog will be automatically generated when using `/ocabot merge $option`.
If you need to run it manually, refer to `OCA/maintainer-tools README <https://github.com/OCA/maintainer-tools>`_.

View File

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import models, wizard

View File

@ -0,0 +1,26 @@
# Copyright (C) 2023 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
{
"name": "Google Drive URL attachment",
"version": "16.0.1.0.0",
"category": "Tools",
"website": "https://github.com/OCA/knowledge",
"author": "Cetmix, Odoo Community Association (OCA)",
"maintainers": ["CetmixGitDrone"],
"license": "AGPL-3",
"installable": True,
"depends": [
"document_url",
],
"data": [
"views/res_users_view.xml",
"views/res_config_settings_view.xml",
],
"assets": {
"web.assets_backend": [
"document_url_google_drive/static/src/js/attachment_google_picker.esm.js",
"document_url_google_drive/static/src/xml/google_picker_url.xml",
"document_url_google_drive/static/src/xml/attachment_google_picker.xml",
],
},
}

View File

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import res_users, res_config_settings

View File

@ -0,0 +1,13 @@
# Copyright (C) 2023 Cetmix OÜ
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
is_active_google_api = fields.Boolean(
string="Google APIs",
config_parameter="document_url_google_drive.is_active_google_api",
)

View File

@ -0,0 +1,51 @@
# Copyright (C) 2023 Cetmix OÜ
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResUsers(models.Model):
_inherit = "res.users"
google_picker_client_id = fields.Char(string="Google Client ID")
google_picker_api_key = fields.Char(string="Google API Key")
google_picker_scope = fields.Char(
string="Google Scope",
default="https://www.googleapis.com/auth/drive.readonly",
)
google_picker_app_id = fields.Char(
string="Google App ID",
default="odoo",
)
google_picker_access_token = fields.Char(string="Google Access Token")
google_picker_mime_types = fields.Char(string="Google Mime Types")
def get_google_picker_params(self):
"""
Get Google Picker params
:return: dict
"""
self.ensure_one()
is_active_google_api = (
self.env["ir.config_parameter"]
.sudo()
.get_param("document_url_google_drive.is_active_google_api")
)
if not is_active_google_api:
return {}
return {
"client_id": self.google_picker_client_id,
"api_key": self.google_picker_api_key,
"scope": self.google_picker_scope,
"app_id": self.google_picker_app_id,
"access_token": self.google_picker_access_token,
"mime_types": self.google_picker_mime_types,
}
def save_google_picker_access_token(self, access_token):
"""
Save Google Picker access token
:param access_token: str
:return: None
"""
self.ensure_one()
self.google_picker_access_token = access_token

View File

@ -0,0 +1,22 @@
To configure this module, you need to:
- Go to Settings -> General Settings and scroll down to the Integrations section.
- Enable "Google API", save.
- Next, open your user profile and set up personal access credentials on the "Google
API" tab.
- field "Google Client ID" - enter the client ID from the Google API console.
- field "Google API key" - enter the API key from the Google API console.
- field "Google Scope" - enter the scope for the Google API. The default value is
`https://www.googleapis.com/auth/drive.readonly`.
- field "Google App ID" - enter the ID of the Google application. The default value is
`odoo`.
- field "Google Access Token" - your token will be displayed here. It is necessary to
edit it.
- field "Google Mime Types" - enter the file formats to be filtered when selecting.
Example: `application/pdf, image/jpeg, image/png`. By default, all files are
selected
- You will be asked to authenticate when you add a link for the first time.

View File

@ -0,0 +1,2 @@
This module extends the functionality of the document_url module and allows you to
attach a link to a file from your Google Drive

View File

@ -0,0 +1,8 @@
To use this module, you need to:
#. Open the form view of an object (Example: Customer Invoice INV/2019/0007). #. Go to
the chatter and click on the attached icon. #. Click **Add GDrive link**. #. Fill the
wizard fields and click on Add button. #. In the open window, select the files you need
and press the select button. #. You will see a new **URL attachment** in the set of
attachments related to the object. #. In order to log in under another google user,
click on the logout icon located after **Add GDrive link**

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,124 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Module name</h2>
<p>This module was written to extend the functionality of ... to support ... and allow you to ...</p>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Installation</h2>
</div>
<div class="oe_span6">
<p class="oe_mt32">To install this module, you need to:
<ul>
<li>...</li>
</ul>
</p>
</div>
<div class="oe_span6">
<div class="oe_demo oe_picture oe_screenshot">
<a href="https://www.odoo.com/saas_master/demo?lang=en_US&module=crm">
<img src="crm_sc_01.png">
</a>
</div>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Configuration</h2>
</div>
<div class="oe_span6">
<p class="oe_mt32">To configure this module, you need to:
<ul>
<li>...</li>
</ul>
</p>
</div>
<div class="oe_span6">
<div class="oe_demo oe_picture oe_screenshot">
<a href="https://www.odoo.com/saas_master/demo?lang=en_US&module=crm">
<img src="crm_sc_01.png">
</a>
</div>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Usage</h2>
</div>
<div class="oe_span6">
<p class="oe_mt32">To use this module, you need to:
<ul>
<li>...</li>
</ul>
</p>
<p class="oe_mt32">For further information, please visit:
<ul>
<li><a href="https://www.odoo.com/forum/help-1">https://www.odoo.com/forum/help-1</a></li>
</ul>
</p>
</div>
<div class="oe_span6">
<div class="oe_demo oe_picture oe_screenshot">
<a href="https://www.odoo.com/saas_master/demo?lang=en_US&module=crm">
<img src="crm_sc_01.png">
</a>
</div>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Known issues / Roadmap</h2>
</div>
<div class="oe_span6">
<p class="oe_mt32">
<ul>
<li>...</li>
</ul>
</p>
</div>
<div class="oe_span6">
<div class="oe_demo oe_picture oe_screenshot">
<a href="https://www.odoo.com/saas_master/demo?lang=en_US&module=crm">
<img src="crm_sc_01.png">
</a>
</div>
</div>
</div>
</section>
<section class="oe_container oe_dark">
<div class="oe_row">
<div class="oe_span12">
<h2 class="oe_slogan">Credits</h2>
</div>
<div class="oe_span12">
<h3>Contributors</h3>
<ul>
<li>Firstname Lastname &lt;<a href="mailto:email.address@example.com">email.address@example.com</a>&gt;</li>
</ul>
</div>
<div class="oe_span12">
<h3>Maintainer</h3>
<p>
This module is maintained by the OCA.<br/>
OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.<br/>
To contribute to this module, please visit <a href="http://odoo-community.org">http://odoo-community.org</a>.<br/>
<a href="http://odoo-community.org"><img class="oe_picture oe_centered" src="http://odoo-community.org/logo.png"></a>
</p>
</div>
</div>
</section>

View File

@ -0,0 +1,179 @@
/** @odoo-module **/
import {loadJS} from "@web/core/assets";
import {registerMessagingComponent} from "@mail/utils/messaging_component";
import {useComponentToModel} from "@mail/component_hooks/use_component_to_model";
import {useService} from "@web/core/utils/hooks";
const {Component, onWillStart, useState} = owl;
export class AttachmentGooglePicker extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({fieldName: "component"});
this.orm = useService("orm");
this.user = useService("user");
this.action = useService("action");
this.state = useState({
pickerInited: false,
gisInited: false,
api_key: "",
scopes: "",
client_id: "",
app_id: "",
accessToken: null,
});
this.tokenClient = null;
onWillStart(async () => {
await this.getUserAuthParams();
if (!this.checkActive()) {
return;
}
await loadJS("https://apis.google.com/js/api.js", {
attrs: {async: true, defer: true},
});
await this.gapiLoaded();
await loadJS("https://accounts.google.com/gsi/client", {
attrs: {async: true, defer: true},
});
await this.gisLoaded();
});
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
_onAddGooglePickerUrl() {
console.log("Add Google Picker URL");
this.handleAuthClick();
}
checkActive() {
return (
Boolean(this.state.api_key) &&
Boolean(this.state.scopes) &&
Boolean(this.state.client_id) &&
Boolean(this.state.app_id)
);
}
async _onSignOut() {
this.state.accessToken = null;
await this.saveUserAuthAccessToken();
}
// --------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------
async getUserAuthParams() {
const res = await this.orm.call("res.users", "get_google_picker_params", [
this.user.userId,
]);
this.state.client_id = res.client_id;
this.state.api_key = res.api_key;
this.state.app_id = res.app_id;
this.state.scopes = res.scope;
this.state.accessToken = res.access_token;
this.state.mime_types = res.mime_types;
}
async saveUserAuthAccessToken() {
this.orm.call("res.users", "save_google_picker_access_token", [
this.user.userId,
this.state.accessToken,
]);
}
async gapiLoaded() {
window.gapi.load("client:picker", this.initializePicker.bind(this));
}
async initializePicker() {
await window.gapi.client.load(
"https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"
);
this.state.pickerInited = true;
}
handleAuthClick() {
this.tokenClient.callback = async (response) => {
if (response.error !== undefined) {
throw response;
}
this.state.accessToken = response.access_token;
await this.createPicker();
await this.saveUserAuthAccessToken();
};
if (this.state.accessToken === null) {
// Prompt the user to select a Google Account and ask for consent to share their data
// when establishing a new session.
this.tokenClient.requestAccessToken({prompt: "consent"});
} else {
// Skip display of account chooser and consent dialog for an existing session.
this.tokenClient.requestAccessToken({prompt: ""});
}
}
async gisLoaded() {
this.tokenClient = window.google.accounts.oauth2.initTokenClient({
client_id: this.state.client_id,
scope: this.state.scopes,
callback: "",
});
this.state.gisInited = true;
}
createPicker() {
const view = new window.google.picker.View(window.google.picker.ViewId.DOCS);
if (this.state.mime_types) {
view.setMimeTypes(this.state.mime_types);
}
const picker = new window.google.picker.PickerBuilder()
.enableFeature(window.google.picker.Feature.NAV_HIDDEN)
.enableFeature(window.google.picker.Feature.MULTISELECT_ENABLED)
.setDeveloperKey(this.state.api_key)
.setAppId(this.state.app_id)
.setOAuthToken(this.state.accessToken)
.addView(view)
.addView(new window.google.picker.DocsUploadView())
.setCallback(this.pickerCallback.bind(this))
.build();
picker.setVisible(true);
}
async pickerCallback(data) {
if (data.action === window.google.picker.Action.PICKED) {
data[window.google.picker.Response.DOCUMENTS].forEach((document) => {
this.createAttachment(document);
});
await this._onAddedUrl();
}
}
async createAttachment(document) {
this.orm.call("ir.attachment.add_url", "add_attachment_google_drive", [
document.url,
document.name,
this.props.record.chatter.thread.model,
[this.props.record.chatter.thread.id],
]);
}
async _onAddedUrl() {
this.props.record.chatter.refresh();
}
}
Object.assign(AttachmentGooglePicker, {
props: {record: Object},
template: "document_url_google_drive.GooglePickerUrl",
});
registerMessagingComponent(AttachmentGooglePicker);

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright (C) 2023 Cetmix OÜ
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<templates xml:space="preserve">
<t t-name="document_url_google_drive.GooglePickerUrl" owl="1">
<button
t-att-style="'display: ' + (checkActive() ? 'inline-block' : 'none') + ';'"
type="button"
class="o_add_url_button btn btn-link"
t-on-click="_onAddGooglePickerUrl"
>
<i class="fa fa-plus-square" />
Add GDrive link
</button>
<i
t-if="state.accessToken"
class="fa fa-sign-out"
aria-hidden="true"
t-on-click="_onSignOut"
/>
</t>
</templates>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright (C) 2023 Cetmix OÜ
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<templates>
<t t-inherit="mail.AttachmentBox" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_AttachmentBox_content')]/div" position="inside">
<AttachmentGooglePicker record="attachmentBoxView" />
</xpath>
</t>
</templates>

View File

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_document_url

View File

@ -0,0 +1,78 @@
# Copyright (C) 2023 Cetmix OÜ
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests import common
class TestDocumentUrl(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.wizard_add_url = cls.env["ir.attachment.add_url"]
cls.config_settings = cls.env["ir.config_parameter"].sudo()
cls.users = cls.env["res.users"]
def test_add_google_drive_url_attachment(self):
self.wizard_add_url.add_attachment_google_drive(
"http://www.odoodemouser.com",
"Demo User (Website)",
"res.users",
[self.env.ref("base.user_demo").id],
)
domain = [
("type", "=", "url"),
("name", "=", "Demo User (Website)"),
("url", "=", "http://www.odoodemouser.com"),
("res_model", "=", "res.users"),
("res_id", "=", self.env.ref("base.user_demo").id),
]
attachment_added_count = self.env["ir.attachment"].search_count(domain)
self.assertEqual(attachment_added_count, 1)
attachment = self.env["ir.attachment"].search(domain)
self.assertEqual(attachment.mimetype, "application/link")
def test_get_google_picker_params_is_active(self):
self.config_settings.set_param(
"document_url_google_drive.is_active_google_api", True
)
user = self.users.with_context(no_reset_password=True).create(
{
"name": "Test User",
"login": "test_user",
"google_picker_client_id": "test_client_id",
"google_picker_api_key": "test_api_key",
"google_picker_scope": "test_scope",
"google_picker_app_id": "test_app_id",
"google_picker_access_token": "test_access_token",
"google_picker_mime_types": "test_mime_types",
}
)
params = user.get_google_picker_params()
self.assertEqual(
params,
{
"client_id": "test_client_id",
"api_key": "test_api_key",
"scope": "test_scope",
"app_id": "test_app_id",
"access_token": "test_access_token",
"mime_types": "test_mime_types",
},
)
def test_get_google_picker_params_not_is_active(self):
self.config_settings.set_param(
"document_url_google_drive.is_active_google_api", False
)
user = self.users.with_context(no_reset_password=True).create(
{
"name": "Test User",
"login": "test_user",
}
)
params = user.get_google_picker_params()
self.assertEqual(
params,
{},
)

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright (C) 2023 Cetmix OÜ
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field
name="name"
>res.config.settings.view.form.inherit.document.url.google.drive</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base_setup.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@id='recaptcha']" position="after">
<div class="col-12 col-lg-6 o_setting_box" id="google_api">
<div class="o_setting_left_pane">
<field name="is_active_google_api" />
</div>
<div class="o_setting_right_pane" id="website_recaptcha_settings">
<label for="is_active_google_api" />
<div class="text-muted">
Use google integration.
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright (C) 2023 Cetmix OÜ
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="res_users_google_picker_view_form" model="ir.ui.view">
<field name="name">res.users (document_url_google_drive)</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 string="Google API">
<group>
<field name="google_picker_client_id" />
<field name="google_picker_api_key" />
<field name="google_picker_scope" />
<field name="google_picker_app_id" />
<field name="google_picker_access_token" readonly="1" />
<field name="google_picker_mime_types" />
</group>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,4 @@
# Copyright (C) 2023 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from . import document_url

View File

@ -0,0 +1,26 @@
# Copyright (C) 2023 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from urllib import parse
from odoo import api, models
class AddUrlWizard(models.TransientModel):
_inherit = "ir.attachment.add_url"
@api.model
def add_attachment_google_drive(self, url, name, active_model, active_ids):
attachment_obj = self.env["ir.attachment"]
url_parse = parse.urlparse(url)
if not url_parse.scheme:
url_parse = parse.urlparse("{}{}".format("http://", url))
for active_id in active_ids:
attachment = {
"name": name,
"type": "url",
"url": url_parse.geturl(),
"res_id": active_id,
"res_model": active_model,
}
attachment_obj.create(attachment)

View File

@ -0,0 +1 @@
../../../../document_url_google_drive

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)