This reverts commit 82f236d98e322efd84c873df54f8e728e1d2a1bf.
The fix is actually not solving all the issues, and also introduce
a behavior change. Actually, the expected result for filters like
<filter string="My Pipeline" name="my" domain="[('user_id', '=', uid)]"/>
will result into a evaluated filters, but invalid as the uid is set to
the person configuring the google configuration, not the user who will
use it.
As it is tricky (or even impossible) to solve the issue properly in all
the cases, we prefer to warn the user that the filter is invalid instead.
closes odoo/odoo#66042
X-original-commit: 5df2c1d10318cc53f1c3e90256cba6a48d246dcb
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
234 lines
12 KiB
Python
234 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import ast
|
|
import logging
|
|
import json
|
|
import re
|
|
|
|
import requests
|
|
import werkzeug.urls
|
|
|
|
from odoo import api, fields, models
|
|
from odoo.exceptions import RedirectWarning, UserError, ValidationError
|
|
from odoo.tools.translate import _
|
|
|
|
from odoo.addons.google_account.models.google_service import GOOGLE_TOKEN_ENDPOINT, TIMEOUT
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class GoogleDrive(models.Model):
|
|
|
|
_name = 'google.drive.config'
|
|
_description = "Google Drive templates config"
|
|
|
|
def get_google_drive_url(self, res_id, template_id):
|
|
self.ensure_one()
|
|
self = self.sudo()
|
|
|
|
model = self.model_id
|
|
filter_name = self.filter_id.name if self.filter_id else False
|
|
record = self.env[model.model].browse(res_id).read()[0]
|
|
record.update({
|
|
'model': model.name,
|
|
'filter': filter_name
|
|
})
|
|
name_gdocs = self.name_template
|
|
try:
|
|
name_gdocs = name_gdocs % record
|
|
except:
|
|
raise UserError(_("At least one key cannot be found in your Google Drive name pattern."))
|
|
|
|
attachments = self.env["ir.attachment"].search([('res_model', '=', model.model), ('name', '=', name_gdocs), ('res_id', '=', res_id)])
|
|
url = False
|
|
if attachments:
|
|
url = attachments[0].url
|
|
else:
|
|
url = self.copy_doc(res_id, template_id, name_gdocs, model.model).get('url')
|
|
return url
|
|
|
|
@api.model
|
|
def get_access_token(self, scope=None):
|
|
Config = self.env['ir.config_parameter'].sudo()
|
|
google_drive_refresh_token = Config.get_param('google_drive_refresh_token')
|
|
user_is_admin = self.env.is_admin()
|
|
if not google_drive_refresh_token:
|
|
if user_is_admin:
|
|
dummy, action_id = self.env['ir.model.data'].get_object_reference('base_setup', 'action_general_configuration')
|
|
msg = _("There is no refresh code set for Google Drive. You can set it up from the configuration panel.")
|
|
raise RedirectWarning(msg, action_id, _('Go to the configuration panel'))
|
|
else:
|
|
raise UserError(_("Google Drive is not yet configured. Please contact your administrator."))
|
|
google_drive_client_id = Config.get_param('google_drive_client_id')
|
|
google_drive_client_secret = Config.get_param('google_drive_client_secret')
|
|
#For Getting New Access Token With help of old Refresh Token
|
|
data = {
|
|
'client_id': google_drive_client_id,
|
|
'refresh_token': google_drive_refresh_token,
|
|
'client_secret': google_drive_client_secret,
|
|
'grant_type': "refresh_token",
|
|
'scope': scope or 'https://www.googleapis.com/auth/drive'
|
|
}
|
|
headers = {"Content-type": "application/x-www-form-urlencoded"}
|
|
try:
|
|
req = requests.post(GOOGLE_TOKEN_ENDPOINT, data=data, headers=headers, timeout=TIMEOUT)
|
|
req.raise_for_status()
|
|
except requests.HTTPError:
|
|
if user_is_admin:
|
|
dummy, action_id = self.env['ir.model.data'].get_object_reference('base_setup', 'action_general_configuration')
|
|
msg = _("Something went wrong during the token generation. Please request again an authorization code .")
|
|
raise RedirectWarning(msg, action_id, _('Go to the configuration panel'))
|
|
else:
|
|
raise UserError(_("Google Drive is not yet configured. Please contact your administrator."))
|
|
return req.json().get('access_token')
|
|
|
|
@api.model
|
|
def copy_doc(self, res_id, template_id, name_gdocs, res_model):
|
|
google_web_base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
|
access_token = self.get_access_token()
|
|
# Copy template in to drive with help of new access token
|
|
request_url = "https://www.googleapis.com/drive/v2/files/%s?fields=parents/id&access_token=%s" % (template_id, access_token)
|
|
headers = {"Content-type": "application/x-www-form-urlencoded"}
|
|
try:
|
|
req = requests.get(request_url, headers=headers, timeout=TIMEOUT)
|
|
req.raise_for_status()
|
|
parents_dict = req.json()
|
|
except requests.HTTPError:
|
|
raise UserError(_("The Google Template cannot be found. Maybe it has been deleted."))
|
|
|
|
record_url = "Click on link to open Record in Odoo\n %s/?db=%s#id=%s&model=%s" % (google_web_base_url, self._cr.dbname, res_id, res_model)
|
|
data = {
|
|
"title": name_gdocs,
|
|
"description": record_url,
|
|
"parents": parents_dict['parents']
|
|
}
|
|
request_url = "https://www.googleapis.com/drive/v2/files/%s/copy?access_token=%s" % (template_id, access_token)
|
|
headers = {
|
|
'Content-type': 'application/json',
|
|
'Accept': 'text/plain'
|
|
}
|
|
# resp, content = Http().request(request_url, "POST", data_json, headers)
|
|
req = requests.post(request_url, data=json.dumps(data), headers=headers, timeout=TIMEOUT)
|
|
req.raise_for_status()
|
|
content = req.json()
|
|
res = {}
|
|
if content.get('alternateLink'):
|
|
res['id'] = self.env["ir.attachment"].create({
|
|
'res_model': res_model,
|
|
'name': name_gdocs,
|
|
'res_id': res_id,
|
|
'type': 'url',
|
|
'url': content['alternateLink']
|
|
}).id
|
|
# Commit in order to attach the document to the current object instance, even if the permissions has not been written.
|
|
self._cr.commit()
|
|
res['url'] = content['alternateLink']
|
|
key = self._get_key_from_url(res['url'])
|
|
request_url = "https://www.googleapis.com/drive/v2/files/%s/permissions?emailMessage=This+is+a+drive+file+created+by+Odoo&sendNotificationEmails=false&access_token=%s" % (key, access_token)
|
|
data = {'role': 'writer', 'type': 'anyone', 'value': '', 'withLink': True}
|
|
try:
|
|
req = requests.post(request_url, data=json.dumps(data), headers=headers, timeout=TIMEOUT)
|
|
req.raise_for_status()
|
|
except requests.HTTPError:
|
|
raise self.env['res.config.settings'].get_config_warning(_("The permission 'reader' for 'anyone with the link' has not been written on the document"))
|
|
if self.env.user.email:
|
|
data = {'role': 'writer', 'type': 'user', 'value': self.env.user.email}
|
|
try:
|
|
requests.post(request_url, data=json.dumps(data), headers=headers, timeout=TIMEOUT)
|
|
except requests.HTTPError:
|
|
pass
|
|
return res
|
|
|
|
@api.model
|
|
def get_google_drive_config(self, res_model, res_id):
|
|
'''
|
|
Function called by the js, when no google doc are yet associated with a record, with the aim to create one. It
|
|
will first seek for a google.docs.config associated with the model `res_model` to find out what's the template
|
|
of google doc to copy (this is usefull if you want to start with a non-empty document, a type or a name
|
|
different than the default values). If no config is associated with the `res_model`, then a blank text document
|
|
with a default name is created.
|
|
:param res_model: the object for which the google doc is created
|
|
:param ids: the list of ids of the objects for which the google doc is created. This list is supposed to have
|
|
a length of 1 element only (batch processing is not supported in the code, though nothing really prevent it)
|
|
:return: the config id and config name
|
|
'''
|
|
# TO DO in master: fix my signature and my model
|
|
if isinstance(res_model, str):
|
|
res_model = self.env['ir.model'].search([('model', '=', res_model)]).id
|
|
if not res_id:
|
|
raise UserError(_("Creating google drive may only be done by one at a time."))
|
|
# check if a model is configured with a template
|
|
configs = self.search([('model_id', '=', res_model)])
|
|
config_values = []
|
|
for config in configs.sudo():
|
|
if config.filter_id:
|
|
if config.filter_id.user_id and config.filter_id.user_id.id != self.env.user.id:
|
|
#Private
|
|
continue
|
|
try:
|
|
domain = [('id', 'in', [res_id])] + ast.literal_eval(config.filter_id.domain)
|
|
except:
|
|
raise UserError(_("The document filter must not include any 'dynamic' part, so it should not be based on the current time or current user, for example."))
|
|
additionnal_context = ast.literal_eval(config.filter_id.context)
|
|
google_doc_configs = self.env[config.filter_id.model_id].with_context(**additionnal_context).search(domain)
|
|
if google_doc_configs:
|
|
config_values.append({'id': config.id, 'name': config.name})
|
|
else:
|
|
config_values.append({'id': config.id, 'name': config.name})
|
|
return config_values
|
|
|
|
name = fields.Char('Template Name', required=True)
|
|
model_id = fields.Many2one('ir.model', 'Model', required=True, ondelete='cascade')
|
|
model = fields.Char('Related Model', related='model_id.model', readonly=True)
|
|
filter_id = fields.Many2one('ir.filters', 'Filter', domain="[('model_id', '=', model)]")
|
|
google_drive_template_url = fields.Char('Template URL', required=True)
|
|
google_drive_resource_id = fields.Char('Resource Id', compute='_compute_ressource_id')
|
|
google_drive_client_id = fields.Char('Google Client', compute='_compute_client_id')
|
|
name_template = fields.Char('Google Drive Name Pattern', default='Document %(name)s', help='Choose how the new google drive will be named, on google side. Eg. gdoc_%(field_name)s', required=True)
|
|
active = fields.Boolean('Active', default=True)
|
|
|
|
def _get_key_from_url(self, url):
|
|
word = re.search("(key=|/d/)([A-Za-z0-9-_]+)", url)
|
|
if word:
|
|
return word.group(2)
|
|
return None
|
|
|
|
def _compute_ressource_id(self):
|
|
for record in self:
|
|
if record.google_drive_template_url:
|
|
word = self._get_key_from_url(record.google_drive_template_url)
|
|
if word:
|
|
record.google_drive_resource_id = word
|
|
else:
|
|
raise UserError(_("Please enter a valid Google Document URL."))
|
|
else:
|
|
record.google_drive_resource_id = False
|
|
|
|
def _compute_client_id(self):
|
|
google_drive_client_id = self.env['ir.config_parameter'].sudo().get_param('google_drive_client_id')
|
|
for record in self:
|
|
record.google_drive_client_id = google_drive_client_id
|
|
|
|
@api.onchange('model_id')
|
|
def _onchange_model_id(self):
|
|
if self.model_id:
|
|
self.model = self.model_id.model
|
|
else:
|
|
self.filter_id = False
|
|
self.model = False
|
|
|
|
@api.constrains('model_id', 'filter_id')
|
|
def _check_model_id(self):
|
|
for drive in self:
|
|
if drive.filter_id and drive.model_id.model != drive.filter_id.model_id:
|
|
raise ValidationError(_(
|
|
"Incoherent Google Drive %(drive)s: the model of the selected filter %(filter)r is not matching the model of current template (%(filter_model)r, %(drive_model)r)",
|
|
drive=drive.name, filter=drive.filter_id.name, filter_model=drive.filter_id.model_id.model, drive_model=drive.model_id.model,
|
|
))
|
|
if self.model_id.model and self.filter_id:
|
|
# force an execution of the filter to verify compatibility
|
|
self.get_google_drive_config(self.model_id.model, 1)
|
|
|
|
def get_google_scope(self):
|
|
return 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file'
|